From e093be066338ce1643440ccfd37a5aca4ce9d9f3 Mon Sep 17 00:00:00 2001 From: "a.bouhuolia" Date: Sun, 31 Jan 2021 13:16:01 +0200 Subject: [PATCH] feat: billing page in dashboard and setup. --- client/src/common/subscriptionModels.js | 42 +- .../Dashboard/DashboardSplitePane.js | 2 +- client/src/components/DialogsContainer.js | 3 + .../Guards/EnsureOrganizationIsReady.js | 6 +- client/src/config/sidebarMenu.js | 4 +- .../src/containers/Authentication/Register.js | 2 +- .../AccountFormDialogContent.js | 2 +- .../PaymentViaVoucherDialogContent.js | 89 +++ .../PaymentViaVoucherForm.js | 75 +++ .../Dialogs/PaymentViaVoucherDialog/index.js | 41 ++ .../Organization/withCurrentOrganization.js | 12 + .../src/containers/Setup/SetupCongratsPage.js | 2 +- client/src/containers/Setup/SetupDialogs.js | 13 + .../containers/Setup/SetupInitializingForm.js | 1 + .../src/containers/Setup/SetupLeftSection.js | 3 + .../containers/Setup/SetupOrganizationForm.js | 625 ++++++------------ .../containers/Setup/SetupOrganizationPage.js | 139 ++++ .../src/containers/Setup/SetupRightSection.js | 141 ++-- .../src/containers/Setup/SetupSubscription.js | 42 ++ .../containers/Setup/SetupSubscriptionForm.js | 97 +-- .../containers/Setup/SetupWizardContent.js | 48 ++ .../Setup/SubscriptionForm.schema.js | 9 + .../containers/Subscriptions/BillingForm.js | 88 +-- .../containers/Subscriptions/BillingPeriod.js | 56 ++ .../Subscriptions/BillingPeriodsInput.js | 42 ++ .../containers/Subscriptions/BillingPlan.js | 57 ++ .../Subscriptions/BillingPlansForm.js | 30 + .../Subscriptions/BillingPlansInput.js | 40 ++ .../containers/Subscriptions/BillingTab.js | 14 +- .../containers/Subscriptions/LicenseTab.js | 49 +- .../Subscriptions/SubscriptionTabs.js | 23 +- .../Subscriptions/billingPaymentmethod.js | 16 +- .../Subscriptions/billingPeriods.js | 69 -- .../containers/Subscriptions/billingPlans.js | 89 --- .../src/containers/Subscriptions/withPlan.js | 17 + .../src/containers/Subscriptions/withPlans.js | 19 + client/src/lang/en/index.js | 13 +- client/src/routes/dashboard.js | 2 +- .../organizations/organizations.actions.js | 2 +- .../organizations/organizations.selectors.js | 2 +- .../store/organizations/withSetupWizard.js | 22 + client/src/store/plans/plans.reducer.js | 52 ++ client/src/store/plans/plans.selectors.js | 29 + client/src/store/reducers.js | 2 + client/src/style/App.scss | 10 + .../style/containers/Dashboard/Sidebar.scss | 6 +- .../src/style/pages/Billing/BillingPage.scss | 26 + client/src/style/pages/Billing/PageForm.scss | 160 ----- client/src/style/pages/Setup/Billing.scss | 0 .../src/style/pages/Setup/Organization.scss | 11 +- .../pages/Setup/PaymentViaVoucherDialog.scss | 23 + client/src/style/pages/Setup/SetupPage.scss | 9 +- .../src/style/pages/Setup/Subscription.scss | 7 + .../pages/Subscription/BillingPlans.scss | 69 ++ .../pages/Subscription/PlanPeriodRadio.scss | 43 ++ .../style/pages/Subscription/PlanRadio.scss | 71 ++ client/src/style/variables.scss | 1 - client/src/utils.js | 6 +- server/src/api/controllers/Settings.ts | 4 +- .../src/api/middleware/SettingsMiddleware.ts | 1 - 60 files changed, 1505 insertions(+), 1073 deletions(-) create mode 100644 client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherDialogContent.js create mode 100644 client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherForm.js create mode 100644 client/src/containers/Dialogs/PaymentViaVoucherDialog/index.js create mode 100644 client/src/containers/Organization/withCurrentOrganization.js create mode 100644 client/src/containers/Setup/SetupDialogs.js create mode 100644 client/src/containers/Setup/SetupOrganizationPage.js create mode 100644 client/src/containers/Setup/SetupSubscription.js create mode 100644 client/src/containers/Setup/SetupWizardContent.js create mode 100644 client/src/containers/Setup/SubscriptionForm.schema.js create mode 100644 client/src/containers/Subscriptions/BillingPeriod.js create mode 100644 client/src/containers/Subscriptions/BillingPeriodsInput.js create mode 100644 client/src/containers/Subscriptions/BillingPlan.js create mode 100644 client/src/containers/Subscriptions/BillingPlansForm.js create mode 100644 client/src/containers/Subscriptions/BillingPlansInput.js delete mode 100644 client/src/containers/Subscriptions/billingPeriods.js delete mode 100644 client/src/containers/Subscriptions/billingPlans.js create mode 100644 client/src/containers/Subscriptions/withPlan.js create mode 100644 client/src/containers/Subscriptions/withPlans.js create mode 100644 client/src/store/organizations/withSetupWizard.js create mode 100644 client/src/store/plans/plans.reducer.js create mode 100644 client/src/store/plans/plans.selectors.js create mode 100644 client/src/style/pages/Billing/BillingPage.scss delete mode 100644 client/src/style/pages/Billing/PageForm.scss delete mode 100644 client/src/style/pages/Setup/Billing.scss create mode 100644 client/src/style/pages/Setup/PaymentViaVoucherDialog.scss create mode 100644 client/src/style/pages/Setup/Subscription.scss create mode 100644 client/src/style/pages/Subscription/BillingPlans.scss create mode 100644 client/src/style/pages/Subscription/PlanPeriodRadio.scss create mode 100644 client/src/style/pages/Subscription/PlanRadio.scss diff --git a/client/src/common/subscriptionModels.js b/client/src/common/subscriptionModels.js index d6f2b530f..205b2c339 100644 --- a/client/src/common/subscriptionModels.js +++ b/client/src/common/subscriptionModels.js @@ -1,41 +1,9 @@ +// Subscription plans. export const plans = [ - { - name: 'basic', - description: [ - 'Sales/purchases module.', - 'Expense module.', - 'Inventory module.', - 'Unlimited status pages.', - 'Unlimited status pages.', - ], - price: '1200', - slug: 'free', - currency: 'LYD', - }, - { - name: 'pro', - description: [ - 'Sales/purchases module.', - 'Expense module.', - 'Inventory module.', - 'Unlimited status pages.', - 'Unlimited status pages.', - ], - price: '1200', - slug: 'free', - currency: 'LYD', - }, + ]; -export const paymentmethod = [ - { - period: 'monthly', - price: '1200', - currency: 'LYD', - }, - { - period: 'yearly', - price: '1200', - currency: 'LYD', - }, +// Payment methods. +export const paymentMethods = [ + ]; diff --git a/client/src/components/Dashboard/DashboardSplitePane.js b/client/src/components/Dashboard/DashboardSplitePane.js index 0abb4ed8b..42433bffb 100644 --- a/client/src/components/Dashboard/DashboardSplitePane.js +++ b/client/src/components/Dashboard/DashboardSplitePane.js @@ -9,7 +9,7 @@ function DashboardSplitPane({ sidebarExpended, children }) { - const initialSize = 200; + const initialSize = 210; const [defaultSize, setDefaultSize] = useState( parseInt(localStorage.getItem('dashboard-size'), 10) || initialSize, diff --git a/client/src/components/DialogsContainer.js b/client/src/components/DialogsContainer.js index bd2027d03..b13dbaabf 100644 --- a/client/src/components/DialogsContainer.js +++ b/client/src/components/DialogsContainer.js @@ -12,6 +12,8 @@ import ReceiptNumberDialog from 'containers/Dialogs/ReceiptNumberDialog'; import InvoiceNumberDialog from 'containers/Dialogs/InvoiceNumberDialog'; import InventoryAdjustmentDialog from 'containers/Dialogs/InventoryAdjustmentFormDialog'; +import PaymentViaVoucherDialog from 'containers/Dialogs/PaymentViaVoucherDialog'; + export default function DialogsContainer() { return (
@@ -26,6 +28,7 @@ export default function DialogsContainer() { +
); } diff --git a/client/src/components/Guards/EnsureOrganizationIsReady.js b/client/src/components/Guards/EnsureOrganizationIsReady.js index 8870a4fed..ba9e8024f 100644 --- a/client/src/components/Guards/EnsureOrganizationIsReady.js +++ b/client/src/components/Guards/EnsureOrganizationIsReady.js @@ -13,9 +13,9 @@ function EnsureOrganizationIsReady({ redirectTo = '/setup', // #withOrganizationByOrgId - isOrganizationInitialized, + isOrganizationReady, }) { - return (isOrganizationInitialized) ? children : ( + return (isOrganizationReady) ? children : ( @@ -27,5 +27,5 @@ export default compose( connect((state, props) => ({ organizationId: props.currentOrganizationId, })), - withOrganization(({ isOrganizationInitialized }) => ({ isOrganizationInitialized })), + withOrganization(({ isOrganizationReady }) => ({ isOrganizationReady })), )(EnsureOrganizationIsReady); \ No newline at end of file diff --git a/client/src/config/sidebarMenu.js b/client/src/config/sidebarMenu.js index 3ed4189f0..d5dea66c3 100644 --- a/client/src/config/sidebarMenu.js +++ b/client/src/config/sidebarMenu.js @@ -161,11 +161,11 @@ export default [ href: '/financial-reports/profit-loss-sheet', }, { - text: 'Receivable Aging Summary', + text: 'A/R Aging Summary', href: '/financial-reports/receivable-aging-summary', }, { - text: 'Payable Aging Summary', + text: 'A/P Aging Summary', href: '/financial-reports/payable-aging-summary', }, ], diff --git a/client/src/containers/Authentication/Register.js b/client/src/containers/Authentication/Register.js index 1b5b69f55..c34cc82ba 100644 --- a/client/src/containers/Authentication/Register.js +++ b/client/src/containers/Authentication/Register.js @@ -101,7 +101,7 @@ function RegisterUserForm({ requestRegister, requestLogin }) { }), }); } - if (errors.some((e) => e.type === 'EMAIL_EXISTS')) { + if (errors.some((e) => e.type === 'EMAIL.EXISTS')) { setErrors({ email: formatMessage({ id: 'the_email_already_used_in_another_account', diff --git a/client/src/containers/Dialogs/AccountFormDialog/AccountFormDialogContent.js b/client/src/containers/Dialogs/AccountFormDialog/AccountFormDialogContent.js index ea5b56201..866ae5425 100644 --- a/client/src/containers/Dialogs/AccountFormDialog/AccountFormDialogContent.js +++ b/client/src/containers/Dialogs/AccountFormDialog/AccountFormDialogContent.js @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { Intent } from '@blueprintjs/core'; import { Formik } from 'formik'; -import { FormattedMessage as T, useIntl } from 'react-intl'; +import { useIntl } from 'react-intl'; import { omit } from 'lodash'; import { useQuery, queryCache } from 'react-query'; import { AppToaster, DialogContent } from 'components'; diff --git a/client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherDialogContent.js b/client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherDialogContent.js new file mode 100644 index 000000000..49f415d16 --- /dev/null +++ b/client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherDialogContent.js @@ -0,0 +1,89 @@ +import React from 'react'; +import { Formik } from 'formik'; + +import { useIntl } from 'react-intl'; +import * as Yup from 'yup'; +import { useHistory } from 'react-router-dom'; + +import 'style/pages/Setup/PaymentViaVoucherDialog.scss'; + + +import { DialogContent } from 'components'; +import PaymentViaLicenseForm from './PaymentViaVoucherForm'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import withBillingActions from 'containers/Subscriptions/withBillingActions'; +import withSubscriptionsActions from 'containers/Subscriptions/withSubscriptionsActions'; + +import { compose } from 'utils'; + +/** + * Payment via license dialog content. + */ +function PaymentViaLicenseDialogContent({ + // #ownProps + subscriptionForm, + + // #withDialog + closeDialog, + + // #withBillingActions + requestSubmitBilling, + + // #withSubscriptionsActions + requestFetchSubscriptions, +}) { + const { formatMessage } = useIntl(); + const history = useHistory(); + + // Handle submit. + const handleSubmit = (values, { setSubmitting }) => { + setSubmitting(true); + + requestSubmitBilling({ ...values, ...subscriptionForm }) + .then(() => { + return requestFetchSubscriptions(); + }) + .then(() => { + return closeDialog('payment-via-voucher'); + }) + .then(() => { + history.push('initializing'); + }) + .finally((errors) => { + setSubmitting(false); + }); + }; + + // Initial values. + const initialValues = { + license_number: '', + plan_slug: '', + period: '', + }; + // Validation schema. + const validationSchema = Yup.object().shape({ + license_number: Yup.string() + .required() + .min(10) + .max(10) + .label(formatMessage({ id: 'license_number' })), + }); + + return ( + + + + ); +} + +export default compose( + withDialogActions, + withBillingActions, + withSubscriptionsActions, +)(PaymentViaLicenseDialogContent); diff --git a/client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherForm.js b/client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherForm.js new file mode 100644 index 000000000..63179d1a4 --- /dev/null +++ b/client/src/containers/Dialogs/PaymentViaVoucherDialog/PaymentViaVoucherForm.js @@ -0,0 +1,75 @@ +import React from 'react'; +import { Button, FormGroup, InputGroup, Intent } from '@blueprintjs/core'; +import { Form, FastField, ErrorMessage } from 'formik'; +import { FormattedMessage as T } from 'react-intl'; +import { compose } from 'redux'; + +import { CLASSES } from 'common/classes'; +import { inputIntent } from 'utils'; +import { useAutofocus } from 'hooks'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; + + +/** + * Payment via license form. + */ +function PaymentViaLicenseForm({ + // #ownProps + isSubmitting, + + // #withDialogActions + closeDialog, +}) { + const licenseNumberRef = useAutofocus(); + + // Handle close button click. + const handleCloseBtnClick = () => { + closeDialog('payment-via-voucher'); + }; + + return ( +
+
+

Please enter your preferred payment method below.

+ + + {({ field, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={'form-group--voucher_number'} + > + (licenseNumberRef.current = ref)} + /> + + )} + +
+ +
+
+ + + +
+
+
+ ); +} + +export default compose( + withDialogActions +)(PaymentViaLicenseForm); \ No newline at end of file diff --git a/client/src/containers/Dialogs/PaymentViaVoucherDialog/index.js b/client/src/containers/Dialogs/PaymentViaVoucherDialog/index.js new file mode 100644 index 000000000..f3870fd97 --- /dev/null +++ b/client/src/containers/Dialogs/PaymentViaVoucherDialog/index.js @@ -0,0 +1,41 @@ +import React, { lazy } from 'react'; +import { Dialog, DialogSuspense } from 'components'; +import { FormattedMessage as T } from 'react-intl'; + +import withDialogRedux from 'components/DialogReduxConnect'; + +import { compose } from 'utils'; + +// Lazy loading the content. +const PaymentViaLicenseDialogContent = lazy(() => import('./PaymentViaVoucherDialogContent')); + +/** + * Payment via license dialog. + */ +function PaymentViaLicenseDialog({ + dialogName, + payload, + isOpen +}) { + return ( + } + className={'dialog--payment-via-voucher'} + autoFocus={true} + canEscapeKeyClose={true} + isOpen={isOpen} + > + + + + + ) +} + +export default compose( + withDialogRedux(), +)(PaymentViaLicenseDialog); \ No newline at end of file diff --git a/client/src/containers/Organization/withCurrentOrganization.js b/client/src/containers/Organization/withCurrentOrganization.js new file mode 100644 index 000000000..b1d1a391f --- /dev/null +++ b/client/src/containers/Organization/withCurrentOrganization.js @@ -0,0 +1,12 @@ +import { connect } from 'react-redux'; + +export default (mapState) => { + const mapStateToProps = (state, props) => { + const mapped = { + organizationTenantId: state.authentication.organizationId, + organizationId: state.authentication.organization, + }; + return (mapState) ? mapState(mapped, state, props) : mapped; + }; + return connect(mapStateToProps); +}; \ No newline at end of file diff --git a/client/src/containers/Setup/SetupCongratsPage.js b/client/src/containers/Setup/SetupCongratsPage.js index 2cb7b87a6..e68334f27 100644 --- a/client/src/containers/Setup/SetupCongratsPage.js +++ b/client/src/containers/Setup/SetupCongratsPage.js @@ -18,7 +18,7 @@ function SetupCongratsPage({ const handleBtnClick = useCallback(() => { setOrganizationSetupCompleted(false); - history.push('/'); + history.push('/homepage'); }, [ setOrganizationSetupCompleted, history, diff --git a/client/src/containers/Setup/SetupDialogs.js b/client/src/containers/Setup/SetupDialogs.js new file mode 100644 index 000000000..ccc5570b1 --- /dev/null +++ b/client/src/containers/Setup/SetupDialogs.js @@ -0,0 +1,13 @@ +import React from 'react'; +import PaymentViaVoucherDialog from 'containers/Dialogs/PaymentViaVoucherDialog'; + +/** + * Setup dialogs. + */ +export default function SetupDialogs() { + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/client/src/containers/Setup/SetupInitializingForm.js b/client/src/containers/Setup/SetupInitializingForm.js index 9445b9354..09569be1a 100644 --- a/client/src/containers/Setup/SetupInitializingForm.js +++ b/client/src/containers/Setup/SetupInitializingForm.js @@ -32,6 +32,7 @@ function SetupInitializingForm({ return (
+

{/* You organization is initializin... */} diff --git a/client/src/containers/Setup/SetupLeftSection.js b/client/src/containers/Setup/SetupLeftSection.js index 41581dec3..329e76ff3 100644 --- a/client/src/containers/Setup/SetupLeftSection.js +++ b/client/src/containers/Setup/SetupLeftSection.js @@ -1,9 +1,12 @@ import React, { useCallback } from 'react'; import { Icon, For } from 'components'; import { FormattedMessage as T } from 'react-intl'; + import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions'; import withAuthentication from 'containers/Authentication/withAuthentication'; + import footerLinks from 'config/footerLinks'; + import { compose } from 'utils'; diff --git a/client/src/containers/Setup/SetupOrganizationForm.js b/client/src/containers/Setup/SetupOrganizationForm.js index 5a9795278..af85afe6b 100644 --- a/client/src/containers/Setup/SetupOrganizationForm.js +++ b/client/src/containers/Setup/SetupOrganizationForm.js @@ -1,7 +1,5 @@ -import React, { useMemo, useState, useCallback } from 'react'; -import * as Yup from 'yup'; -import { useFormik } from 'formik'; -import { Row, Col } from 'react-grid-system'; +import React from 'react'; +import { FastField, Form, ErrorMessage } from 'formik'; import { Button, Intent, @@ -11,429 +9,218 @@ import { Classes, Position, } from '@blueprintjs/core'; -import moment from 'moment'; +import { DateInput } from '@blueprintjs/datetime'; import classNames from 'classnames'; import { TimezonePicker } from '@blueprintjs/timezone'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import { DateInput } from '@blueprintjs/datetime'; -import { withWizard } from 'react-albus'; +import { FormattedMessage as T } from 'react-intl'; -import 'style/pages/Setup/Organization.scss'; +import { Col, Row, ListSelect } from 'components'; +import { + momentFormatter, + tansformDateValue, + inputIntent, + handleDateChange +} from 'utils'; -import { momentFormatter, tansformDateValue } from 'utils'; -import { ListSelect, ErrorMessage, FieldRequiredHint } from 'components'; +import fiscalYearOptions from 'common/fiscalYearOptions'; +import languages from 'common/languagesOptions'; +import currencies from 'common/currencies'; -import withSettingsActions from 'containers/Settings/withSettingsActions'; -import withOrganizationActions from 'containers/Organization/withOrganizationActions'; - -import { compose, optionsMapToArray } from 'utils'; - -function SetupOrganizationForm({ - requestSubmitOptions, - requestOrganizationSeed, - wizard, - setOrganizationSetupCompleted -}) { - const { formatMessage } = useIntl(); - const [selected, setSelected] = useState(); - - const baseCurrency = [ - { id: 0, name: 'LYD - Libyan Dinar', value: 'LYD' }, - { id: 1, name: 'USD - American Dollar', value: 'USD' }, - ]; - - const languages = [ - { id: 0, name: 'English', value: 'en' }, - { id: 1, name: 'Arabic', value: 'ar' }, - ]; - - 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 ValidationSchema = Yup.object().shape({ - name: Yup.string() - .required() - .label(formatMessage({ id: 'organization_name_' })), - financial_date_start: Yup.date() - .required() - .label(formatMessage({ id: 'date_start_' })), - base_currency: Yup.string() - .required() - .label(formatMessage({ id: 'base_currency_' })), - language: Yup.string() - .required() - .label(formatMessage({ id: 'language' })), - fiscal_year: Yup.string() - .required() - .label(formatMessage({ id: 'fiscal_year_' })), - time_zone: Yup.string() - .required() - .label(formatMessage({ id: 'time_zone_' })), - }); - - const initialValues = useMemo( - () => ({ - name: '', - financial_date_start: moment(new Date()).format('YYYY-MM-DD'), - base_currency: '', - language: '', - fiscal_year: '', - time_zone: '', - }), - [], - ); - - const { - values, - errors, - touched, - handleSubmit, - setFieldValue, - getFieldProps, - isSubmitting, - } = useFormik({ - enableReinitialize: true, - validationSchema: ValidationSchema, - initialValues: { - ...initialValues, - }, - onSubmit: (values, { setSubmitting, setErrors }) => { - const options = optionsMapToArray(values).map((option) => { - return { key: option.key, ...option, group: 'organization' }; - }); - requestSubmitOptions({ options }) - .then(() => { - return requestOrganizationSeed(); - }) - .then(() => { - return setOrganizationSetupCompleted(true); - }) - .then((response) => { - setSubmitting(false); - wizard.next(); - }) - .catch((erros) => { - setSubmitting(false); - }); - }, - }); - - const onItemsSelect = (filedName) => { - return (filed) => { - setSelected({ - ...selected, - [filedName]: filed, - }); - 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 onItemRenderer = (item, { handleClick }) => ( - - ); - - const handleTimeZoneChange = useCallback( - (time_zone) => { - setFieldValue('time_zone', time_zone); - }, - [setFieldValue], - ); - - const handleDateChange = useCallback( - (date) => { - const formatted = moment(date).format('YYYY-MM-DD'); - setFieldValue('financial_date_start', formatted); - }, - [setFieldValue], - ); +/** + * Setup organization form. + */ +export default function SetupOrganizationForm({ isSubmitting, values }) { return ( -
-
-

- -

-

- -

-
+
+

+ +

- -

- -

- - } - labelInfo={} - className={'form-group--name'} - intent={errors.name && touched.name && Intent.DANGER} - helperText={} - > - - - - {/* financial starting date */} - } - labelInfo={} - intent={ - errors.financial_date_start && - touched.financial_date_start && - Intent.DANGER - } - helperText={ - - } - className={classNames('form-group--select-list', Classes.FILL)} - > - - - - {/* base currency */} - - } - labelInfo={} - className={classNames( - 'form-group--base-currency', - 'form-group--select-list', - Classes.LOADING, - Classes.FILL, - )} - intent={ - errors.base_currency && touched.base_currency && Intent.DANGER - } - helperText={ - - } - > - } - itemRenderer={onItemRenderer} - popoverProps={{ minimal: true }} - onItemSelect={onItemsSelect('base_currency')} - itemPredicate={filterItems} - selectedItem={values.base_currency} - selectedItemProp={'value'} - defaultText={} - textProp={'name'} - /> - - - - {/* language */} - - } - labelInfo={} - 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={} - textProp={'name'} - /> - - - - {/* fiscal Year */} - } - labelInfo={} - className={classNames( - 'form-group--fiscal_year', - 'form-group--select-list', - Classes.FILL, - )} - intent={errors.fiscal_year && touched.fiscal_year && Intent.DANGER} - helperText={ - - } - > - } - itemRenderer={onItemRenderer} - popoverProps={{ minimal: true }} - onItemSelect={onItemsSelect('fiscal_year')} - itemPredicate={filterItems} - selectedItem={values.fiscal_year} - selectedItemProp={'value'} - defaultText={} - textProp={'name'} - /> - - - {/* Time zone */} - } - labelInfo={} - className={classNames( - 'form-group--time-zone', - 'form-group--select-list', - Classes.FILL, - )} - intent={errors.time_zone && touched.time_zone && Intent.DANGER} - helperText={ - - } - > - } - /> - - -

- -

-
- -
-
-
+ + + )} + + + {/* ---------- Financial starting date ---------- */} + + {({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => ( + } + intent={inputIntent({ error, touched })} + helperText={} + className={classNames('form-group--select-list', Classes.FILL)} + > + { + setFieldValue('financialDateStart', formattedDate); + })} + /> + + )} + + + + + {/* ---------- Base currency ---------- */} + + {({ + form: { setFieldValue }, + field: { value }, + meta: { error, touched }, + }) => ( + } + className={classNames( + 'form-group--base-currency', + 'form-group--select-list', + Classes.FILL, + )} + intent={inputIntent({ error, touched })} + helperText={} + > + } + popoverProps={{ minimal: true }} + onItemSelect={(item) => { + setFieldValue('baseCurrency', item.code); + }} + selectedItemProp={'code'} + textProp={'label'} + defaultText={} + selectedItem={value} + /> + + )} + + + + {/* ---------- Language ---------- */} + + + {({ + form: { setFieldValue }, + field: { value }, + meta: { error, touched }, + }) => ( + } + className={classNames( + 'form-group--language', + 'form-group--select-list', + Classes.FILL, + )} + intent={inputIntent({ error, touched })} + helperText={} + > + } + onItemSelect={(item) => { + setFieldValue('language', item.value); + }} + selectedItem={value} + textProp={'name'} + selectedItemProp={'value'} + defaultText={} + popoverProps={{ minimal: true }} + /> + + )} + + + + {/* --------- Fiscal Year ----------- */} + + {({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => ( + } + className={classNames( + 'form-group--fiscal_year', + 'form-group--select-list', + Classes.FILL, + )} + intent={inputIntent({ error, touched })} + helperText={} + > + } + selectedItem={value} + selectedItemProp={'value'} + textProp={'name'} + defaultText={} + popoverProps={{ minimal: true }} + onItemSelect={(item) => { + setFieldValue('fiscalYear', item.value) + }} + /> + + )} + + + {/* ---------- Time zone ---------- */} + + {({ + form: { setFieldValue }, + field: { value }, + meta: { error, touched }, + }) => ( + } + className={classNames( + 'form-group--time-zone', + 'form-group--select-list', + Classes.FILL, + )} + intent={inputIntent({ error, touched })} + helperText={} + > + { + setFieldValue('timeZone', item); + }} + valueDisplayFormat="composite" + showLocalTimezone={true} + placeholder={} + popoverProps={{ minimal: true }} + /> + + )} + + +

+ +

+ +
+ +
+ ); } - -export default compose( - withSettingsActions, - withOrganizationActions, - withWizard, -)(SetupOrganizationForm); diff --git a/client/src/containers/Setup/SetupOrganizationPage.js b/client/src/containers/Setup/SetupOrganizationPage.js new file mode 100644 index 000000000..7aa562e75 --- /dev/null +++ b/client/src/containers/Setup/SetupOrganizationPage.js @@ -0,0 +1,139 @@ +import React from 'react'; +import * as Yup from 'yup'; +import { Formik } from 'formik'; +import moment from 'moment'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { snakeCase } from 'lodash'; + +import { withWizard } from 'react-albus'; +import { useQuery } from 'react-query'; + +import 'style/pages/Setup/Organization.scss'; + +import SetupOrganizationForm from './SetupOrganizationForm'; + +import withSettingsActions from 'containers/Settings/withSettingsActions'; +import withSettings from 'containers/Settings/withSettings'; +import withOrganizationActions from 'containers/Organization/withOrganizationActions'; + +import { + compose, + transformToForm, + optionsMapToArray, +} from 'utils'; + +/** + * Setup organization form. + */ +function SetupOrganizationPage({ + // #withSettingsActions + requestSubmitOptions, + requestFetchOptions, + + // #withOrganizationActions + requestOrganizationSeed, + + // #withSettings + organizationSettings, + + wizard, + setOrganizationSetupCompleted, +}) { + const { formatMessage } = useIntl(); + + const fetchSettings = useQuery(['settings'], () => requestFetchOptions({})); + + // Validation schema. + const validationSchema = Yup.object().shape({ + name: Yup.string() + .required() + .label(formatMessage({ id: 'organization_name_' })), + financialDateStart: Yup.date() + .required() + .label(formatMessage({ id: 'date_start_' })), + baseCurrency: Yup.string() + .required() + .label(formatMessage({ id: 'base_currency_' })), + language: Yup.string() + .required() + .label(formatMessage({ id: 'language' })), + fiscalYear: Yup.string() + .required() + .label(formatMessage({ id: 'fiscal_year_' })), + timeZone: Yup.string() + .required() + .label(formatMessage({ id: 'time_zone_' })), + }); + + // Initial values. + const defaultValues = { + name: '', + financialDateStart: moment(new Date()).format('YYYY-MM-DD'), + baseCurrency: '', + language: '', + fiscalYear: '', + timeZone: '', + ...organizationSettings, + }; + + const initialValues = { + ...defaultValues, + + /** + * We only care about the fields in the form. Previously unfilled optional + * values such as `notes` come back from the API as null, so remove those + * as well. + */ + ...transformToForm(organizationSettings, defaultValues), + }; + + // Handle the form submit. + const handleSubmit = (values, { setSubmitting, setErrors }) => { + const options = optionsMapToArray(values).map((option) => { + return { ...option, key: snakeCase(option.key), group: 'organization' }; + }); + requestSubmitOptions({ options }) + .then(() => { + return requestOrganizationSeed(); + }) + .then(() => { + return setOrganizationSetupCompleted(true); + }) + .then((response) => { + setSubmitting(false); + wizard.next(); + }) + .catch((erros) => { + setSubmitting(false); + }); + }; + + return ( +
+
+

+ +

+

+ +

+
+ + +
+ ); +} + +export default compose( + withSettingsActions, + withOrganizationActions, + withWizard, + withSettings(({ organizationSettings }) => ({ + organizationSettings, + })), +)(SetupOrganizationPage); diff --git a/client/src/containers/Setup/SetupRightSection.js b/client/src/containers/Setup/SetupRightSection.js index 3913fbaba..e1d784419 100644 --- a/client/src/containers/Setup/SetupRightSection.js +++ b/client/src/containers/Setup/SetupRightSection.js @@ -1,56 +1,52 @@ -import React, { useCallback } from 'react'; -import { TransitionGroup, CSSTransition } from 'react-transition-group'; -import { Wizard, Steps, Step } from 'react-albus'; -import { useHistory } from "react-router-dom"; -import { connect } from 'react-redux'; +import React from 'react'; + +import { Wizard } from 'react-albus'; +import { useHistory } from 'react-router-dom'; -import WizardSetupSteps from './WizardSetupSteps'; import withSubscriptions from 'containers/Subscriptions/withSubscriptions'; -import SetupSubscriptionForm from './SetupSubscriptionForm'; -import SetupOrganizationForm from './SetupOrganizationForm'; -import SetupInitializingForm from './SetupInitializingForm'; -import SetupCongratsPage from './SetupCongratsPage'; +import SetupDialogs from './SetupDialogs'; +import SetupWizardContent from './SetupWizardContent'; + +import withOrganization from 'containers/Organization/withOrganization'; +import withCurrentOrganization from 'containers/Organization/withCurrentOrganization'; +import withSetupWizard from '../../store/organizations/withSetupWizard'; -import withAuthentication from 'containers/Authentication/withAuthentication'; -import withOrganization from 'containers/Organization/withOrganization' import { compose } from 'utils'; /** * Wizard setup right section. */ -function SetupRightSection ({ - // #withAuthentication - currentOrganizationId, - +function SetupRightSection({ // #withOrganization isOrganizationInitialized, isOrganizationSeeded, isOrganizationSetupCompleted, + // #withSetupWizard + isCongratsStep, + isSubscriptionStep, + isInitializingStep, + isOrganizationStep, + // #withSubscriptions - isSubscriptionActive + isSubscriptionActive, }) { const history = useHistory(); - const handleSkip = useCallback(({ step, push }) => { + const handleSkip = ({ step, push }) => { const scenarios = [ - { condition: isOrganizationSetupCompleted, redirectTo: 'congrats' }, - { condition: !isSubscriptionActive, redirectTo: 'subscription' }, - { condition: isSubscriptionActive && !isOrganizationInitialized, redirectTo: 'initializing' }, - { condition: isSubscriptionActive && !isOrganizationSeeded, redirectTo: 'organization' }, + { condition: isCongratsStep, redirectTo: 'congrats' }, + { condition: isSubscriptionStep, redirectTo: 'subscription' }, + { condition: isInitializingStep, redirectTo: 'initializing' }, + { condition: isOrganizationStep, redirectTo: 'organization' }, ]; const scenario = scenarios.find((scenario) => scenario.condition); if (scenario) { push(scenario.redirectTo); } - }, [ - isSubscriptionActive, - isOrganizationInitialized, - isOrganizationSeeded, - isOrganizationSetupCompleted - ]); + }; return (
@@ -58,58 +54,47 @@ function SetupRightSection ({ onNext={handleSkip} basename={'/setup'} history={history} - render={({ step, steps }) => ( -
- - - - -
- - - - - - - - - - - - - - - - - -
-
-
-
- )} /> + render={SetupWizardContent} + /> +
- ) + ); } export default compose( - withAuthentication(({ currentOrganizationId }) => ({ currentOrganizationId })), - connect((state, props) => ({ - organizationId: props.currentOrganizationId, + withCurrentOrganization(({ organizationTenantId }) => ({ + organizationId: organizationTenantId, })), - withOrganization(({ - organization, - isOrganizationInitialized, - isOrganizationSeeded, - isOrganizationSetupCompleted - }) => ({ - organization, - isOrganizationInitialized, - isOrganizationSeeded, - isOrganizationSetupCompleted - })), - withSubscriptions(({ - isSubscriptionActive, - }) => ({ - isSubscriptionActive - }), 'main'), -)(SetupRightSection); \ No newline at end of file + withOrganization( + ({ + organization, + isOrganizationInitialized, + isOrganizationSeeded, + isOrganizationSetupCompleted, + }) => ({ + organization, + isOrganizationInitialized, + isOrganizationSeeded, + isOrganizationSetupCompleted, + }), + ), + withSubscriptions( + ({ isSubscriptionActive }) => ({ + isSubscriptionActive, + }), + 'main', + ), + withSetupWizard( + ({ + isCongratsStep, + isSubscriptionStep, + isInitializingStep, + isOrganizationStep, + }) => ({ + isCongratsStep, + isSubscriptionStep, + isInitializingStep, + isOrganizationStep, + }), + ), +)(SetupRightSection); diff --git a/client/src/containers/Setup/SetupSubscription.js b/client/src/containers/Setup/SetupSubscription.js new file mode 100644 index 000000000..e13cd0f19 --- /dev/null +++ b/client/src/containers/Setup/SetupSubscription.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { Formik } from 'formik'; +import { withWizard } from 'react-albus'; + +import 'style/pages/Setup/Subscription.scss'; + +import SetupSubscriptionForm from './SetupSubscriptionForm'; +import { SubscriptionFormSchema } from './SubscriptionForm.schema'; + +import { compose } from 'utils'; + +/** + * Subscription step of wizard setup. + */ +function SetupSubscription({ + // #withWizard + wizard, +}) { + // Initial values. + const initialValues = { + plan_slug: 'free', + period: 'month', + license_code: '', + }; + + const handleSubmit = () => {}; + + return ( +
+ +
+ ); +} + +export default compose( + withWizard, +)(SetupSubscription); diff --git a/client/src/containers/Setup/SetupSubscriptionForm.js b/client/src/containers/Setup/SetupSubscriptionForm.js index 5d26dfbb3..6116e5e97 100644 --- a/client/src/containers/Setup/SetupSubscriptionForm.js +++ b/client/src/containers/Setup/SetupSubscriptionForm.js @@ -1,98 +1,15 @@ -import React, { useMemo } from 'react'; -import * as Yup from 'yup'; -import { useFormik } from 'formik'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import { Button, Intent } from '@blueprintjs/core'; -import { withWizard } from 'react-albus'; +import React from 'react'; +import { Form } from 'formik'; -import 'style/pages/Setup/Billing.scss'; - -import BillingPlans from 'containers/Subscriptions/billingPlans'; -import BillingPeriods from 'containers/Subscriptions/billingPeriods'; -import { BillingPaymentmethod } from 'containers/Subscriptions/billingPaymentmethod'; - -import withSubscriptionsActions from 'containers/Subscriptions/withSubscriptionsActions'; -import withBillingActions from 'containers/Subscriptions/withBillingActions'; - -import { compose } from 'utils'; +import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm'; /** * Subscription step of wizard setup. */ -function SetupSubscriptionForm({ - // #withBillingActions - requestSubmitBilling, - - // #withWizard - wizard, - - // #withSubscriptionsActions - requestFetchSubscriptions -}) { - const { formatMessage } = useIntl(); - const validationSchema = Yup.object().shape({ - plan_slug: Yup.string() - .required() - .label(formatMessage({ id: 'plan_slug' })), - license_code: Yup.string() - .min(10) - .max(10) - .required() - .label(formatMessage({ id: 'license_code_' })) - .trim(), - }); - - const initialValues = useMemo( - () => ({ - plan_slug: '', - license_code: '', - }), - [], - ); - - const formik = useFormik({ - enableReinitialize: true, - validationSchema: validationSchema, - initialValues: { - ...initialValues, - }, - onSubmit: (values, { setSubmitting, setErrors }) => { - requestSubmitBilling(values) - .then((response) => { - return requestFetchSubscriptions(); - }) - .then(() => { - wizard.next(); - setSubmitting(false); - }) - .catch((errors) => { - setSubmitting(false); - }); - }, - }); +export default function SetupSubscriptionForm() { return ( -
-
- - - - -
- -
- -
+
+ + ); } - -export default compose( - withBillingActions, - withWizard, - withSubscriptionsActions, -)(SetupSubscriptionForm); diff --git a/client/src/containers/Setup/SetupWizardContent.js b/client/src/containers/Setup/SetupWizardContent.js new file mode 100644 index 000000000..1b14e9ddb --- /dev/null +++ b/client/src/containers/Setup/SetupWizardContent.js @@ -0,0 +1,48 @@ +import React from 'react'; +import { Steps, Step } from 'react-albus'; +import { TransitionGroup, CSSTransition } from 'react-transition-group'; + +import WizardSetupSteps from './WizardSetupSteps'; + +import SetupSubscription from './SetupSubscription'; +import SetupOrganizationPage from './SetupOrganizationPage'; +import SetupInitializingForm from './SetupInitializingForm'; +import SetupCongratsPage from './SetupCongratsPage'; + +/** + * Setup wizard content. + */ +export default function SetupWizardContent({ + step, + steps +}) { + return ( +
+ + + + +
+ + + + + + + + + + + + + + + + + +
+
+
+
+ ); +} diff --git a/client/src/containers/Setup/SubscriptionForm.schema.js b/client/src/containers/Setup/SubscriptionForm.schema.js new file mode 100644 index 000000000..c5e055523 --- /dev/null +++ b/client/src/containers/Setup/SubscriptionForm.schema.js @@ -0,0 +1,9 @@ +import * as Yup from 'yup'; +import { formatMessage } from 'services/intl'; + +export const SubscriptionFormSchema = Yup.object().shape({ + plan_slug: Yup.string() + .required() + .label(formatMessage({ id: 'plan_slug' })), + period: Yup.string().required(), +}); \ No newline at end of file diff --git a/client/src/containers/Subscriptions/BillingForm.js b/client/src/containers/Subscriptions/BillingForm.js index 828d84a42..53b8207b2 100644 --- a/client/src/containers/Subscriptions/BillingForm.js +++ b/client/src/containers/Subscriptions/BillingForm.js @@ -1,13 +1,21 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect } from 'react'; import * as Yup from 'yup'; -import { useFormik } from 'formik'; -import { Button, Intent } from '@blueprintjs/core'; +import { Formik, Form } from 'formik'; import { FormattedMessage as T, useIntl } from 'react-intl'; -import withDashboardActions from 'containers/Dashboard/withDashboardActions'; -import { MeteredBillingTabs, PaymentMethodTabs } from './SubscriptionTabs'; +import DashboardInsider from 'components/Dashboard/DashboardInsider'; + +import 'style/pages/Billing/BillingPage.scss'; + +import { MasterBillingTabs } from './SubscriptionTabs'; + import withBillingActions from './withBillingActions'; +import withDashboardActions from 'containers/Dashboard/withDashboardActions'; + import { compose } from 'utils'; +/** + * Billing form. + */ function BillingForm({ // #withDashboardActions changePageTitle, @@ -23,51 +31,43 @@ function BillingForm({ const validationSchema = Yup.object().shape({ plan_slug: Yup.string() - .required() - .label(formatMessage({ id: 'plan_slug' })), + .required(), + period: Yup.string().required(), license_code: Yup.string().trim(), }); - const initialValues = useMemo( - () => ({ - plan_slug: 'free', - license_code: '', - }), - [], - ); + const initialValues = { + plan_slug: 'free', + period: 'month', + license_code: '', + }; - const formik = useFormik({ - enableReinitialize: true, - validationSchema: validationSchema, - initialValues: { - ...initialValues, - }, - onSubmit: (values, { setSubmitting, resetForm, setErrors }) => { - requestSubmitBilling(values) - .then((response) => { - setSubmitting(false); - }) - .catch((errors) => { - setSubmitting(false); - }); - }, - }); + const handleSubmit = (values, { setSubmitting }) => { + requestSubmitBilling(values) + .then((response) => { + setSubmitting(false); + }) + .catch((errors) => { + setSubmitting(false); + }); + }; return ( -
-
- -
- -
- -
+ +
+ + {({ isSubmitting, handleSubmit }) => ( +
+ + + )} +
+
+
); } diff --git a/client/src/containers/Subscriptions/BillingPeriod.js b/client/src/containers/Subscriptions/BillingPeriod.js new file mode 100644 index 000000000..4b3b33922 --- /dev/null +++ b/client/src/containers/Subscriptions/BillingPeriod.js @@ -0,0 +1,56 @@ +import React from 'react'; +import classNames from 'classnames'; +import { get } from 'lodash'; +import { compose } from 'redux'; + +import 'style/pages/Subscription/PlanPeriodRadio.scss'; + +import withPlan from 'containers/Subscriptions/withPlan'; + +import { saveInvoke } from 'utils'; + +/** + * Billing period. + */ +function BillingPeriod({ + // #ownProps + label, + currencyCode, + value, + selectedOption, + onSelected, + period, + price, +}) { + const handlePeriodClick = () => { + saveInvoke(onSelected, value); + }; + return ( +
+ {label} + +
+ + {price} {currencyCode} + + {label} +
+
+ ); +} + +export default compose( + withPlan(({ plan }, state, { period }) => ({ + price: get(plan, `price.${period}`), + currencyCode: get(plan, 'currencyCode'), + })), +)(BillingPeriod); diff --git a/client/src/containers/Subscriptions/BillingPeriodsInput.js b/client/src/containers/Subscriptions/BillingPeriodsInput.js new file mode 100644 index 000000000..8421bb8c7 --- /dev/null +++ b/client/src/containers/Subscriptions/BillingPeriodsInput.js @@ -0,0 +1,42 @@ +import React from 'react'; +import { Field } from 'formik'; +import BillingPeriod from './BillingPeriod'; + +import withPlans from './withPlans'; + +import { compose } from 'utils'; + +/** + * Billing periods. + */ +function BillingPeriods({ title, description, plansPeriods }) { + return ( +
+

{title}

+
+

{description}

+
+ + + {({ form: { setFieldValue, values } }) => ( +
+ {plansPeriods.map((period) => ( + setFieldValue('period', value)} + selectedOption={values.period} + /> + ))} +
+ )} +
+
+ ); +} + +export default compose(withPlans(({ plansPeriods }) => ({ plansPeriods })))( + BillingPeriods, +); diff --git a/client/src/containers/Subscriptions/BillingPlan.js b/client/src/containers/Subscriptions/BillingPlan.js new file mode 100644 index 000000000..cc6223f49 --- /dev/null +++ b/client/src/containers/Subscriptions/BillingPlan.js @@ -0,0 +1,57 @@ +import React from 'react'; +import classNames from 'classnames'; +import { FormattedMessage as T } from 'react-intl'; + +import 'style/pages/Subscription/PlanRadio.scss'; + +import { saveInvoke } from 'utils'; + +/** + * Billing plan. + */ +export default function BillingPlan({ + name, + description, + price, + currencyCode, + + value, + selectedOption, + onSelected, +}) { + const handlePlanClick = () => { + saveInvoke(onSelected, value); + }; + return ( +
+
+
+ +
+
+ +
+
    + {description.map((line) => ( +
  • {line}
  • + ))} +
+
+ +
+ + {price} {currencyCode} + + + + +
+
+ ); +} \ No newline at end of file diff --git a/client/src/containers/Subscriptions/BillingPlansForm.js b/client/src/containers/Subscriptions/BillingPlansForm.js new file mode 100644 index 000000000..789bba008 --- /dev/null +++ b/client/src/containers/Subscriptions/BillingPlansForm.js @@ -0,0 +1,30 @@ +import React from 'react'; +import { FormattedMessage as T } from 'react-intl'; + +import 'style/pages/Subscription/BillingPlans.scss' + +import BillingPlansInput from 'containers/Subscriptions/BillingPlansInput'; +import BillingPeriodsInput from 'containers/Subscriptions/BillingPeriodsInput'; +import BillingPaymentMethod from 'containers/Subscriptions/BillingPaymentMethod'; + +/** + * Billing plans form. + */ +export default function BillingPlansForm() { + return ( +
+ } + description={} + /> + } + description={} + /> + } + description={} + /> +
+ ) +} \ No newline at end of file diff --git a/client/src/containers/Subscriptions/BillingPlansInput.js b/client/src/containers/Subscriptions/BillingPlansInput.js new file mode 100644 index 000000000..540493cc8 --- /dev/null +++ b/client/src/containers/Subscriptions/BillingPlansInput.js @@ -0,0 +1,40 @@ +import React from 'react'; +import { FastField } from 'formik'; +import BillingPlan from './BillingPlan'; + +import withPlans from './withPlans'; +import { compose } from 'utils'; + +/** + * Billing plans. + */ +function BillingPlans({ plans, title, description, selectedOption }) { + return ( +
+

{title}

+
+

{description}

+
+ + + {({ form: { setFieldValue }, field: { value } }) => ( +
+ {plans.map((plan) => ( + setFieldValue('plan_slug', value)} + selectedOption={value} + /> + ))} +
+ )} +
+
+ ); +} +export default compose(withPlans(({ plans }) => ({ plans })))(BillingPlans); diff --git a/client/src/containers/Subscriptions/BillingTab.js b/client/src/containers/Subscriptions/BillingTab.js index 7b8db9bd5..36c3abe06 100644 --- a/client/src/containers/Subscriptions/BillingTab.js +++ b/client/src/containers/Subscriptions/BillingTab.js @@ -1,16 +1,10 @@ import React from 'react'; -import BillingPlans from 'containers/Subscriptions/billingPlans'; -import BillingPeriods from 'containers/Subscriptions/billingPeriods'; -import { BillingPaymentmethod } from 'containers/Subscriptions/billingPaymentmethod'; +import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm'; -function BillingTab({ formik }) { +export default function BillingTab() { return (
- - - +
); -} - -export default BillingTab; +} \ No newline at end of file diff --git a/client/src/containers/Subscriptions/LicenseTab.js b/client/src/containers/Subscriptions/LicenseTab.js index 6df07c5ec..8143a4fab 100644 --- a/client/src/containers/Subscriptions/LicenseTab.js +++ b/client/src/containers/Subscriptions/LicenseTab.js @@ -1,35 +1,38 @@ import React from 'react'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import { InputGroup, FormGroup, Intent } from '@blueprintjs/core'; -import ErrorMessage from 'components/ErrorMessage'; +import { FormattedMessage as T } from 'react-intl'; + +import { Intent, Button } from '@blueprintjs/core'; + +import withDialogActions from 'containers/Dialog/withDialogActions'; +import { compose } from 'redux'; +import { useFormikContext } from 'formik'; + +/** + * Payment via license code tab. + */ +function LicenseTab({ openDialog }) { + const { submitForm, values } = useFormikContext(); + + const handleSubmitBtnClick = () => { + submitForm().then(() => { + openDialog('payment-via-voucher', { ...values }); + }); + }; -function LicenseTab({ - formik: { errors, touched, setFieldValue, getFieldProps }, -}) { return (
-

- -

+

+ +

- } - intent={errors.license_code && touched.license_code && Intent.DANGER} - helperText={ - - } - className={'form-group-license_code'} - > - - +
); } -export default LicenseTab; +export default compose(withDialogActions)(LicenseTab); diff --git a/client/src/containers/Subscriptions/SubscriptionTabs.js b/client/src/containers/Subscriptions/SubscriptionTabs.js index 7113fbc6b..f0ee09f0b 100644 --- a/client/src/containers/Subscriptions/SubscriptionTabs.js +++ b/client/src/containers/Subscriptions/SubscriptionTabs.js @@ -1,16 +1,18 @@ -import React, { useState } from 'react'; +import React from 'react'; import { Tabs, Tab } from '@blueprintjs/core'; -import { FormattedMessage as T, useIntl } from 'react-intl'; +import { useIntl } from 'react-intl'; import BillingTab from './BillingTab'; import LicenseTab from './LicenseTab'; -export const MeteredBillingTabs = ({ formik }) => { - const [animate, setAnimate] = useState(true); +/** + * Master billing tabs. + */ +export const MasterBillingTabs = ({ formik }) => { const { formatMessage } = useIntl(); return (
- + { title={formatMessage({ id: 'usage' })} id={'usage'} disabled={true} - // panel={'Usage'} />
); }; +/** + * Payment methods tabs. + */ export const PaymentMethodTabs = ({ formik }) => { - const [animate, setAnimate] = useState(true); const { formatMessage } = useIntl(); return (
- + } /> { +export default ({ formik, title, description }) => { return ( -
-

- -

-

- -

+
+

{ title }

+

{ description }

+
); diff --git a/client/src/containers/Subscriptions/billingPeriods.js b/client/src/containers/Subscriptions/billingPeriods.js deleted file mode 100644 index 9454bdaff..000000000 --- a/client/src/containers/Subscriptions/billingPeriods.js +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import classNames from 'classnames'; -import { paymentmethod } from 'common/subscriptionModels'; - -function BillingPeriod({ price, period, currency, onSelected, selected }) { - return ( - - - - - -
- - {price} {currency} - - - - -
-
- ); -} -function BillingPeriods({ formik, title, selected = 1 }) { - const billingRef = useRef(null); - - useEffect(() => { - const billingPriod = billingRef.current.querySelectorAll('a'); - const billingSelected = billingRef.current.querySelector( - '.billing-selected', - ); - billingPriod.forEach((el) => { - el.addEventListener('click', () => { - billingSelected.classList.remove('billing-selected'); - el.classList.add('billing-selected'); - }); - }); - }); - - return ( -
-

- -

-

- -

-
- {paymentmethod.map((pay, index) => ( - formik.setFieldValue('period', pay.period)} - selected={selected == index + 1} - /> - ))} -
-
- ); -} - -export default BillingPeriods; diff --git a/client/src/containers/Subscriptions/billingPlans.js b/client/src/containers/Subscriptions/billingPlans.js deleted file mode 100644 index f94e14c1e..000000000 --- a/client/src/containers/Subscriptions/billingPlans.js +++ /dev/null @@ -1,89 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { FormattedMessage as T, useIntl } from 'react-intl'; -import classNames from 'classnames'; -import { plans } from 'common/subscriptionModels'; - -function BillingPlan({ - name, - description, - price, - slug, - currency, - onSelected, - selected, -}) { - return ( - onSelected(slug)} - > -
-
- -
-
-
-
    - {description.map((desc, index) => ( -
  • {desc}
  • - ))} -
-
-
- - {' '} - {price} {currency} - - - - -
-
- ); -} - -function BillingPlans({ formik, title, selected = 1 }) { - const planRef = useRef(null); - - useEffect(() => { - const plans = planRef.current.querySelectorAll('a'); - const planSelected = planRef.current.querySelector('.plan-selected'); - - plans.forEach((el) => { - el.addEventListener('click', () => { - planSelected.classList.remove('plan-selected'); - el.classList.add('plan-selected'); - }); - }); - }); - - return ( -
-

- -

-

- -

-
- {plans.map((plan, index) => ( - formik.setFieldValue('plan_slug', plan.slug)} - selected={selected == index + 1} - /> - ))} -
-
- ); -} - -export default BillingPlans; - diff --git a/client/src/containers/Subscriptions/withPlan.js b/client/src/containers/Subscriptions/withPlan.js new file mode 100644 index 000000000..59cf1befc --- /dev/null +++ b/client/src/containers/Subscriptions/withPlan.js @@ -0,0 +1,17 @@ + + + +import { connect } from 'react-redux'; +import { getPlanSelector } from 'store/plans/plans.selectors'; + +export default (mapState) => { + const mapStateToProps = (state, props) => { + const getPlan = getPlanSelector(); + + const mapped = { + plan: getPlan(state, props), + }; + return (mapState) ? mapState(mapped, state, props) : mapped; + }; + return connect(mapStateToProps); +}; \ No newline at end of file diff --git a/client/src/containers/Subscriptions/withPlans.js b/client/src/containers/Subscriptions/withPlans.js new file mode 100644 index 000000000..38ca3f5c5 --- /dev/null +++ b/client/src/containers/Subscriptions/withPlans.js @@ -0,0 +1,19 @@ +import { connect } from 'react-redux'; +import { + getPlansSelector, + getPlansPeriodsSelector, +} from 'store/plans/plans.selectors'; + +export default (mapState) => { + const mapStateToProps = (state, props) => { + const getPlans = getPlansSelector(); + const getPlansPeriods = getPlansPeriodsSelector(); + + const mapped = { + plans: getPlans(state, props), + plansPeriods: getPlansPeriods(state, props), + }; + return mapState ? mapState(mapped, state, props) : mapped; + }; + return connect(mapStateToProps); +}; diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index fb3a36431..d31656aa4 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -353,8 +353,8 @@ export default { once_delete_these_exchange_rates_you_will_not_able_restore_them: `Once you delete these exchange rates, you won't be able to retrieve them later. Are you sure you want to delete them?`, once_delete_this_item_category_you_will_able_to_restore_it: `Once you delete this category, you won\'t be able to restore it later. Are you sure you want to delete this item?`, select_business_location: 'Select Business Location', - select_base_currency: 'Select Base Currency', - select_fiscal_year: 'Select Fiscal Year', + select_base_currency: 'Select base currency', + select_fiscal_year: 'Select fiscal year', select_language: 'Select Language', select_date_format: 'Select Date Format', select_time_zone: 'Select Time Zone', @@ -700,9 +700,9 @@ export default { billing: 'Billing', the_billing_has_been_created_successfully: 'The billing has been created successfully.', - a_select_a_plan: 'A. Select a plan', - b_choose_your_billing: 'B. Choose your billing', - c_payment_methods: 'C. Payment methods', + select_a_plan: '{order}. Select a plan', + choose_your_billing: '{order}. Choose your billing', + payment_methods: '{order}. Payment methods', usage: 'Usage', basic: 'Basic', license: 'License', @@ -964,4 +964,7 @@ export default { selected_customers: '{count} Selected Customers', transaction_number: 'Transaction #', running_balance: 'Running balance', + payment_via_voucher: 'Payment via voucher', + voucher_number: 'Voucher number', + voucher: 'Voucher' }; diff --git a/client/src/routes/dashboard.js b/client/src/routes/dashboard.js index 9516d827b..15c40a3fb 100644 --- a/client/src/routes/dashboard.js +++ b/client/src/routes/dashboard.js @@ -404,5 +404,5 @@ export default [ loader: () => import('containers/Purchases/PaymentMades/PaymentMadeList'), }), breadcrumb: 'Payment Made List', - }, + } ]; diff --git a/client/src/store/organizations/organizations.actions.js b/client/src/store/organizations/organizations.actions.js index d3db2ba7f..ec44a62d4 100644 --- a/client/src/store/organizations/organizations.actions.js +++ b/client/src/store/organizations/organizations.actions.js @@ -40,11 +40,11 @@ export const seedTenant = () => (dispatch, getState) => new Promise((resolve, re payload: { organizationId } }); ApiService.post(`organization/seed/`).then((response) => { - resolve(response); dispatch({ type: t.SET_ORGANIZATION_SEEDED, payload: { organizationId } }); + resolve(response); }) .catch((error) => { reject(error.response.data.errors || []); diff --git a/client/src/store/organizations/organizations.selectors.js b/client/src/store/organizations/organizations.selectors.js index 197ca0254..931ed6f53 100644 --- a/client/src/store/organizations/organizations.selectors.js +++ b/client/src/store/organizations/organizations.selectors.js @@ -54,4 +54,4 @@ export const isOrganizationCongratsFactory = () => createSelector( (organization) => { return !!organization?.is_congrats; } -) \ No newline at end of file +); diff --git a/client/src/store/organizations/withSetupWizard.js b/client/src/store/organizations/withSetupWizard.js new file mode 100644 index 000000000..19a265244 --- /dev/null +++ b/client/src/store/organizations/withSetupWizard.js @@ -0,0 +1,22 @@ +import { connect } from 'react-redux'; + +export default (mapState) => { + const mapStateToProps = (state, props) => { + const { + isOrganizationSetupCompleted, + isOrganizationInitialized, + isOrganizationSeeded, + + isSubscriptionActive + } = props; + + const mapped = { + isCongratsStep: isOrganizationSetupCompleted, + isSubscriptionStep: !isSubscriptionActive, + isInitializingStep: isSubscriptionActive && !isOrganizationInitialized, + isOrganizationStep: isOrganizationInitialized && !isOrganizationSeeded, + }; + return mapState ? mapState(mapped, state, props) : mapped; + }; + return connect(mapStateToProps); +}; diff --git a/client/src/store/plans/plans.reducer.js b/client/src/store/plans/plans.reducer.js new file mode 100644 index 000000000..5f661231a --- /dev/null +++ b/client/src/store/plans/plans.reducer.js @@ -0,0 +1,52 @@ +import { createReducer } from '@reduxjs/toolkit'; + +const initialState = { + plans: [ + { + name: 'Free', + slug: 'free', + description: [ + 'Sales/purchases module.', + 'Expense module.', + 'Inventory module.', + 'Unlimited status pages.', + 'Unlimited status pages.', + ], + price: { + month: '100', + year: '1200', + }, + currencyCode: 'LYD', + }, + { + name: 'Pro', + slug: 'pro', + description: [ + 'Sales/purchases module.', + 'Expense module.', + 'Inventory module.', + 'Unlimited status pages.', + 'Unlimited status pages.', + ], + price: { + month: '200', + year: '2400', + }, + currencyCode: 'LYD', + }, + ], + periods: [ + { + slug: 'month', + label: 'Monthly', + }, + { + slug: 'year', + label: 'Yearly', + }, + ], +}; + +export default createReducer(initialState, { + +}); diff --git a/client/src/store/plans/plans.selectors.js b/client/src/store/plans/plans.selectors.js new file mode 100644 index 000000000..c2ea94478 --- /dev/null +++ b/client/src/store/plans/plans.selectors.js @@ -0,0 +1,29 @@ +import { createSelector } from 'reselect'; + +const plansSelector = (state) => state.plans.plans; +const planSelector = (state, props) => state.plans.plans + .find((plan) => plan.slug === props.planSlug); + +const plansPeriodsSelector = (state) => state.plans.periods; + +// Retrieve manual jounral current page results. +export const getPlansSelector = () => createSelector( + plansSelector, + (plans) => { + return plans; + }, +); + +// Retrieve manual jounral current page results. +export const getPlansPeriodsSelector = () => createSelector( + plansPeriodsSelector, + (periods) => { + return periods; + }, +); + +// Retrieve plan details. +export const getPlanSelector = () => createSelector( + planSelector, + (plan) => plan, +) \ No newline at end of file diff --git a/client/src/store/reducers.js b/client/src/store/reducers.js index db7b467d6..d74669c5f 100644 --- a/client/src/store/reducers.js +++ b/client/src/store/reducers.js @@ -28,6 +28,7 @@ import paymentMades from './PaymentMades/paymentMade.reducer'; import organizations from './organizations/organizations.reducers'; import subscriptions from './subscription/subscription.reducer'; import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer'; +import plans from './plans/plans.reducer'; export default combineReducers({ authentication, @@ -58,4 +59,5 @@ export default combineReducers({ paymentReceives, paymentMades, inventoryAdjustments, + plans }); diff --git a/client/src/style/App.scss b/client/src/style/App.scss index 7fd1a8d23..afdb18d4e 100644 --- a/client/src/style/App.scss +++ b/client/src/style/App.scss @@ -95,4 +95,14 @@ body.hide-scrollbar .Pane2{ button{ justify-content: start; } +} + +.bp3-timezone-picker{ + + .bp3-button{ + + [icon="caret-down"] { + display: none; + } + } } \ No newline at end of file diff --git a/client/src/style/containers/Dashboard/Sidebar.scss b/client/src/style/containers/Dashboard/Sidebar.scss index ec2bb8e77..3d6fb2a28 100644 --- a/client/src/style/containers/Dashboard/Sidebar.scss +++ b/client/src/style/containers/Dashboard/Sidebar.scss @@ -57,7 +57,7 @@ .#{$ns}-menu-item { color: $sidebar-menu-item-color; border-radius: 0; - padding: 8px 18px; + padding: 8px 20px; font-size: 15px; font-weight: 400; @@ -84,7 +84,7 @@ display: block; color: $sidebar-menu-label-color; font-size: 11px; - padding: 10px 18px; + padding: 10px 20px; margin-top: 4px; text-transform: uppercase; font-weight: 500; @@ -104,7 +104,7 @@ padding-top: 6px; } .#{$ns}-menu-item { - padding: 8px 16px; + padding: 8px 20px; font-size: 15px; color: $sidebar-submenu-item-color; diff --git a/client/src/style/pages/Billing/BillingPage.scss b/client/src/style/pages/Billing/BillingPage.scss new file mode 100644 index 000000000..3583e6898 --- /dev/null +++ b/client/src/style/pages/Billing/BillingPage.scss @@ -0,0 +1,26 @@ + + +.billing-page{ + padding: 0 60px; + margin-top: 20px; + max-width: 820px; + + .bp3-tab-list{ + border-bottom: 2px solid #d7e1e7; + + .bp3-tab[aria-disabled="true"]{ + color: rgba(92, 112, 128, 0.7); + } + } + .plan-radio, + .period-radio{ + background: transparent; + border-color: #bbcad4; + + &.is-selected{ + background: #f1f3fb; + border-color: #0269ff + } + } + +} \ No newline at end of file diff --git a/client/src/style/pages/Billing/PageForm.scss b/client/src/style/pages/Billing/PageForm.scss deleted file mode 100644 index 5783dd714..000000000 --- a/client/src/style/pages/Billing/PageForm.scss +++ /dev/null @@ -1,160 +0,0 @@ -.billing-form { - padding: 25px 45px; - width: 800px; - margin: 0 auto; - - .billing-section{ - margin-bottom: 30px; - } - - .paragraph + .billing-form__plan-container{ - margin-top: 20px; - } - - &__plan-container { - display: flex; - flex-flow: row wrap; - - .plan-wrapper { - display: flex; - flex-direction: column; - width: 215px; - height: 267px; - border-radius: 5px; - padding: 15px; - border: 1px solid #dcdcdc; - background: #fcfdff; - text-decoration: none; - color: #000; - cursor: pointer; - - &.plan-wrapper:not(:first-child) { - margin-left: 20px; - } - - .plan-header { - display: flex; - justify-content: flex-start; - } - .plan-name { - background: #3657ff; - border-radius: 3px; - padding: 2px 8px 2px 8px; - font-size: 13px; - color: #fff; - margin-bottom: 16px; - height: 21px; - } - .plan-description { - font-size: 14px; - font-weight: 400; - line-height: 1.8rem; - - &.plan-description ul { - list-style: none; - } - } - .plan-price { - margin-top: auto; - - .amount { - font-weight: 500; - } - - .period { - font-weight: 400; - color: #666; - &.period::before { - content: '/'; - display: inline-block; - margin: 0 2px; - } - } - } - } - a.plan-selected { - border: 1px solid #0069ff; - background-color: #fcfdff; - } - } - - .paragraph + .payment-method-continer{ - margin-top: 20px; - } - - .payment-method-continer { - margin-bottom: 30px; - - .period-container { - display: inline-flex; - background-color: #fcfdff; - justify-content: space-between; - align-items: center; - width: 215px; - height: 36px; - border-radius: 5px; - padding: 8px 10px; - color: #000; - border: 1px solid #dcdcdc; - cursor: pointer; - text-decoration: none; - &.period-container:not(:first-child) { - margin-left: 20px; - } - .period::before { - content: '/'; - display: inline-block; - margin: 0 2px; - } - .bg-period { - font-size: 14px; - font-weight: 500; - } - } - a.billing-selected { - border: 1px solid #0069ff; - background-color: #fcfdff; - } - } - - .license-container { - - .form-group-license_code{ - margin-top: 20px; - } - - .bp3-form-content { - .bp3-input-group { - display: block; - position: relative; - } - .bp3-input { - position: relative; - width: 59%; - height: 41px; - } - } - h4 { - font-size: 18px; - font-weight: 400; - color: #444444; - } - p { - margin-top: 15px; - font-size: 14px; - } - } - - .bp3-tab-list { - border-bottom: 2px solid #f1f1f1; - width: 95%; - } - .subscribe-button { - .bp3-button { - background-color: #0063ff; - min-height: 41px; - width: 240px; - // width: 25%; - } - } -} diff --git a/client/src/style/pages/Setup/Billing.scss b/client/src/style/pages/Setup/Billing.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/style/pages/Setup/Organization.scss b/client/src/style/pages/Setup/Organization.scss index 8dad04218..67f43d203 100644 --- a/client/src/style/pages/Setup/Organization.scss +++ b/client/src/style/pages/Setup/Organization.scss @@ -5,7 +5,7 @@ padding: 45px 0 20px; &__title-wrap { - margin-bottom: 32px; + margin-bottom: 20px; h1 { margin-top: 0; @@ -17,17 +17,24 @@ } } - &__form { + form { h3 { margin-bottom: 1rem; } } .bp3-form-group { + margin-bottom: 24px; + .bp3-input-group { .bp3-input { height: 38px; } + + } + .bp3-input, + .form-group--select-list .bp3-button{ + font-size: 15px; } } diff --git a/client/src/style/pages/Setup/PaymentViaVoucherDialog.scss b/client/src/style/pages/Setup/PaymentViaVoucherDialog.scss new file mode 100644 index 000000000..722f2abcd --- /dev/null +++ b/client/src/style/pages/Setup/PaymentViaVoucherDialog.scss @@ -0,0 +1,23 @@ + + +.dialog--payment-via-voucher{ + + .bp3-dialog-body{ + p{ + margin-bottom: 20px; + } + .form-group-license_code{ + margin-bottom: 30px; + } + } + + .bp3-dialog-footer-actions{ + button{ + min-width: 80px; + + &.bp3-intent-primary{ + min-width: 90px; + } + } + } +} \ No newline at end of file diff --git a/client/src/style/pages/Setup/SetupPage.scss b/client/src/style/pages/Setup/SetupPage.scss index 73f2bd8b4..7393911ed 100644 --- a/client/src/style/pages/Setup/SetupPage.scss +++ b/client/src/style/pages/Setup/SetupPage.scss @@ -59,7 +59,7 @@ &__title { font-size: 26px; - font-weight: 700; + font-weight: 600; line-height: normal; margin-bottom: 20px; margin-top: 14px; @@ -131,11 +131,9 @@ margin: 0 auto; padding: 50px 0 0; } - ul { display: flex; } - li { position: relative; list-style-type: none; @@ -155,7 +153,6 @@ border-radius: 50%; background-color: #75859c; } - &::after { width: 100%; height: 2px; @@ -166,23 +163,19 @@ left: -50%; z-index: -1; } - &:first-child::after { display: none; } - &.is-active { &::before { background-color: #75859c; } - ~ li { &:before, &:after { background: #ebebeb; } } - p.wizard-info { color: #004dd0; } diff --git a/client/src/style/pages/Setup/Subscription.scss b/client/src/style/pages/Setup/Subscription.scss new file mode 100644 index 000000000..fa07c25c9 --- /dev/null +++ b/client/src/style/pages/Setup/Subscription.scss @@ -0,0 +1,7 @@ + +.setup-subscription-form{ + max-width: 800px; + margin: 0 auto; + padding: 0 60px; + margin-top: 40px; +} diff --git a/client/src/style/pages/Subscription/BillingPlans.scss b/client/src/style/pages/Subscription/BillingPlans.scss new file mode 100644 index 000000000..aaa828753 --- /dev/null +++ b/client/src/style/pages/Subscription/BillingPlans.scss @@ -0,0 +1,69 @@ + +.billing-plans{ + + &__section{ + margin-bottom: 35px; + + .title{ + font-size: 22px; + font-weight: 500; + color: #6b7382; + margin-top: 0; + margin-bottom: 12px; + } + .bp3-tab-list { + border-bottom: 2px solid #e6e6e6; + width: 95%; + + .bp3-tab-indicator-wrapper .bp3-tab-indicator{ + bottom: -2px; + } + } + .bp3-tab-panel{ + margin-top: 26px; + } + .subscribe-button { + .bp3-button { + background-color: #0063ff; + min-height: 41px; + width: 240px; + } + } + .plan-radios, + .plan-periods{ + margin-top: 20px; + } + } + + .license-container { + + .bp3-button{ + margin-top: 14px; + padding: 0 30px; + } + .form-group-license_code{ + margin-top: 20px; + } + .bp3-form-content { + .bp3-input-group { + display: block; + position: relative; + } + .bp3-input { + position: relative; + width: 59%; + height: 41px; + } + } + h4 { + font-size: 18px; + font-weight: 400; + color: #444444; + } + p { + margin-top: 15px; + font-size: 14px; + } + } + +} diff --git a/client/src/style/pages/Subscription/PlanPeriodRadio.scss b/client/src/style/pages/Subscription/PlanPeriodRadio.scss new file mode 100644 index 000000000..186d11e85 --- /dev/null +++ b/client/src/style/pages/Subscription/PlanPeriodRadio.scss @@ -0,0 +1,43 @@ + + +// Plan period radio component. +// --------------------- +.period-radios{ + display: flex; +} +.period-radio{ + display: inline-flex; + background-color: #fcfdff; + justify-content: space-between; + align-items: center; + width: 240px; + height: 36px; + border-radius: 5px; + padding: 8px 10px; + color: #000; + border: 1px solid #dcdcdc; + cursor: pointer; + text-decoration: none; + + &.is-selected { + border: 1px solid #0069ff; + background-color: #fcfdff; + } + &:not(:first-child) { + margin-left: 20px; + } + &__amount{ + font-weight: 600; + } + &__period{ + color: #666; + font-size: 14px; + font-weight: 500; + + &::before { + content: '/'; + display: inline-block; + margin: 0 2px; + } + } +} diff --git a/client/src/style/pages/Subscription/PlanRadio.scss b/client/src/style/pages/Subscription/PlanRadio.scss new file mode 100644 index 000000000..5615bd53a --- /dev/null +++ b/client/src/style/pages/Subscription/PlanRadio.scss @@ -0,0 +1,71 @@ + + +// Plan radio component. +// --------------------- +.plan-radios{ + display: flex; +} +.plan-radio { + display: flex; + flex-direction: column; + width: 215px; + height: 267px; + border-radius: 5px; + padding: 15px; + border: 1px solid #dcdcdc; + background: #fcfdff; + text-decoration: none; + color: #000; + cursor: pointer; + + &.is-selected { + border: 1px solid #0069ff; + background-color: #fcfdff; + } + &:not(:first-child) { + margin-left: 20px; + } + &__header { + display: flex; + justify-content: flex-start; + } + &__name { + background: #3657ff; + border-radius: 3px; + padding: 2px 8px 2px 8px; + font-size: 13px; + color: #fff; + margin-bottom: 16px; + height: 21px; + text-transform: uppercase; + } + &__description { + font-size: 14px; + font-weight: 400; + + ul { + list-style: none; + + li{ + margin-bottom: 9px; + } + } + } + &__price { + margin-top: auto; + font-size: 15px; + } + &__amount { + font-weight: 600; + } + &__period { + font-weight: 400; + color: #666; + + &::before { + content: '/'; + display: inline-block; + margin: 0 2px; + } + } +} \ No newline at end of file diff --git a/client/src/style/variables.scss b/client/src/style/variables.scss index 6f5e33918..060f0ec2b 100644 --- a/client/src/style/variables.scss +++ b/client/src/style/variables.scss @@ -28,7 +28,6 @@ $button-background-color-disabled: #E9ECEF !default; $button-background-color: #E6EFFB !default; $button-background-color-hover: #CFDCEE !default; - $sidebar-background: #01115e; $sidebar-text-color: #fff; $sidebar-width: 100%; diff --git a/client/src/utils.js b/client/src/utils.js index a68ea7bae..b9df865c6 100644 --- a/client/src/utils.js +++ b/client/src/utils.js @@ -407,6 +407,9 @@ export const transformToCamelCase = (object) => { return deepMapKeys(object, (key) => _.snakeCase(key)); }; +export const transfromToSnakeCase = (object) => { + return deepMapKeys(object, (key) => _.snakeCase(key)); +}; export function flatObject(obj) { const flatObject = {}; @@ -427,4 +430,5 @@ export function flatObject(obj) { dig(obj); return flatObject; -} \ No newline at end of file +} + diff --git a/server/src/api/controllers/Settings.ts b/server/src/api/controllers/Settings.ts index 23b5b1136..b849d5767 100644 --- a/server/src/api/controllers/Settings.ts +++ b/server/src/api/controllers/Settings.ts @@ -79,7 +79,7 @@ export default class SettingsController extends BaseController{ * @param {Request} req - * @param {Response} res - */ - saveSettings(req: Request, res: Response) { + async saveSettings(req: Request, res: Response) { const { Option } = req.models; const optionsDTO: IOptionsDTO = this.matchedBodyData(req); const { settings } = req; @@ -102,6 +102,8 @@ export default class SettingsController extends BaseController{ }); this.observeAppConfigsComplete(settings); + await settings.save(); + return res.status(200).send({ type: 'success', code: 'OPTIONS.SAVED.SUCCESSFULLY', diff --git a/server/src/api/middleware/SettingsMiddleware.ts b/server/src/api/middleware/SettingsMiddleware.ts index faa1055c0..51e47f2b2 100644 --- a/server/src/api/middleware/SettingsMiddleware.ts +++ b/server/src/api/middleware/SettingsMiddleware.ts @@ -4,7 +4,6 @@ import SettingsStore from 'services/Settings/SettingsStore'; export default async (req: Request, res: Response, next: NextFunction) => { const { tenantId } = req.user; - const { knex } = req; const Logger = Container.get('logger'); const tenantContainer = Container.of(`tenant-${tenantId}`);