mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-20 23:00:34 +00:00
feat: billing page in dashboard and setup.
This commit is contained in:
@@ -1,41 +1,9 @@
|
|||||||
|
// Subscription plans.
|
||||||
export const 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 = [
|
// Payment methods.
|
||||||
{
|
export const paymentMethods = [
|
||||||
period: 'monthly',
|
|
||||||
price: '1200',
|
|
||||||
currency: 'LYD',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
period: 'yearly',
|
|
||||||
price: '1200',
|
|
||||||
currency: 'LYD',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ function DashboardSplitPane({
|
|||||||
sidebarExpended,
|
sidebarExpended,
|
||||||
children
|
children
|
||||||
}) {
|
}) {
|
||||||
const initialSize = 200;
|
const initialSize = 210;
|
||||||
|
|
||||||
const [defaultSize, setDefaultSize] = useState(
|
const [defaultSize, setDefaultSize] = useState(
|
||||||
parseInt(localStorage.getItem('dashboard-size'), 10) || initialSize,
|
parseInt(localStorage.getItem('dashboard-size'), 10) || initialSize,
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import ReceiptNumberDialog from 'containers/Dialogs/ReceiptNumberDialog';
|
|||||||
import InvoiceNumberDialog from 'containers/Dialogs/InvoiceNumberDialog';
|
import InvoiceNumberDialog from 'containers/Dialogs/InvoiceNumberDialog';
|
||||||
import InventoryAdjustmentDialog from 'containers/Dialogs/InventoryAdjustmentFormDialog';
|
import InventoryAdjustmentDialog from 'containers/Dialogs/InventoryAdjustmentFormDialog';
|
||||||
|
|
||||||
|
import PaymentViaVoucherDialog from 'containers/Dialogs/PaymentViaVoucherDialog';
|
||||||
|
|
||||||
export default function DialogsContainer() {
|
export default function DialogsContainer() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -26,6 +28,7 @@ export default function DialogsContainer() {
|
|||||||
<ExchangeRateFormDialog dialogName={'exchangeRate-form'} />
|
<ExchangeRateFormDialog dialogName={'exchangeRate-form'} />
|
||||||
<ItemCategoryDialog dialogName={'item-category-form'} />
|
<ItemCategoryDialog dialogName={'item-category-form'} />
|
||||||
<InventoryAdjustmentDialog dialogName={'inventory-adjustment-form'} />
|
<InventoryAdjustmentDialog dialogName={'inventory-adjustment-form'} />
|
||||||
|
<PaymentViaVoucherDialog dialogName={'payment-via-voucher'} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ function EnsureOrganizationIsReady({
|
|||||||
redirectTo = '/setup',
|
redirectTo = '/setup',
|
||||||
|
|
||||||
// #withOrganizationByOrgId
|
// #withOrganizationByOrgId
|
||||||
isOrganizationInitialized,
|
isOrganizationReady,
|
||||||
}) {
|
}) {
|
||||||
return (isOrganizationInitialized) ? children : (
|
return (isOrganizationReady) ? children : (
|
||||||
<Redirect
|
<Redirect
|
||||||
to={{ pathname: redirectTo }}
|
to={{ pathname: redirectTo }}
|
||||||
/>
|
/>
|
||||||
@@ -27,5 +27,5 @@ export default compose(
|
|||||||
connect((state, props) => ({
|
connect((state, props) => ({
|
||||||
organizationId: props.currentOrganizationId,
|
organizationId: props.currentOrganizationId,
|
||||||
})),
|
})),
|
||||||
withOrganization(({ isOrganizationInitialized }) => ({ isOrganizationInitialized })),
|
withOrganization(({ isOrganizationReady }) => ({ isOrganizationReady })),
|
||||||
)(EnsureOrganizationIsReady);
|
)(EnsureOrganizationIsReady);
|
||||||
@@ -161,11 +161,11 @@ export default [
|
|||||||
href: '/financial-reports/profit-loss-sheet',
|
href: '/financial-reports/profit-loss-sheet',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Receivable Aging Summary',
|
text: 'A/R Aging Summary',
|
||||||
href: '/financial-reports/receivable-aging-summary',
|
href: '/financial-reports/receivable-aging-summary',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Payable Aging Summary',
|
text: 'A/P Aging Summary',
|
||||||
href: '/financial-reports/payable-aging-summary',
|
href: '/financial-reports/payable-aging-summary',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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({
|
setErrors({
|
||||||
email: formatMessage({
|
email: formatMessage({
|
||||||
id: 'the_email_already_used_in_another_account',
|
id: 'the_email_already_used_in_another_account',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Intent } from '@blueprintjs/core';
|
import { Intent } from '@blueprintjs/core';
|
||||||
import { Formik } from 'formik';
|
import { Formik } from 'formik';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { omit } from 'lodash';
|
import { omit } from 'lodash';
|
||||||
import { useQuery, queryCache } from 'react-query';
|
import { useQuery, queryCache } from 'react-query';
|
||||||
import { AppToaster, DialogContent } from 'components';
|
import { AppToaster, DialogContent } from 'components';
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<DialogContent>
|
||||||
|
<Formik
|
||||||
|
initialValues={initialValues}
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
component={PaymentViaLicenseForm}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withDialogActions,
|
||||||
|
withBillingActions,
|
||||||
|
withSubscriptionsActions,
|
||||||
|
)(PaymentViaLicenseDialogContent);
|
||||||
@@ -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 (
|
||||||
|
<Form>
|
||||||
|
<div className={CLASSES.DIALOG_BODY}>
|
||||||
|
<p>Please enter your preferred payment method below.</p>
|
||||||
|
|
||||||
|
<FastField name="license_number">
|
||||||
|
{({ field, meta: { error, touched } }) => (
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'voucher_number'} />}
|
||||||
|
intent={inputIntent({ error, touched })}
|
||||||
|
helperText={<ErrorMessage name="voucher_number" />}
|
||||||
|
className={'form-group--voucher_number'}
|
||||||
|
>
|
||||||
|
<InputGroup
|
||||||
|
{...field}
|
||||||
|
inputRef={(ref) => (licenseNumberRef.current = ref)}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</FastField>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={CLASSES.DIALOG_FOOTER}>
|
||||||
|
<div className={CLASSES.DIALOG_FOOTER_ACTIONS}>
|
||||||
|
<Button onClick={handleCloseBtnClick} disabled={isSubmitting}>
|
||||||
|
<T id={'close'} />
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
disabled={false}
|
||||||
|
type="submit"
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
<T id={'submit'} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withDialogActions
|
||||||
|
)(PaymentViaLicenseForm);
|
||||||
@@ -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 (
|
||||||
|
<Dialog
|
||||||
|
name={dialogName}
|
||||||
|
title={<T id={'payment_via_voucher'} />}
|
||||||
|
className={'dialog--payment-via-voucher'}
|
||||||
|
autoFocus={true}
|
||||||
|
canEscapeKeyClose={true}
|
||||||
|
isOpen={isOpen}
|
||||||
|
>
|
||||||
|
<DialogSuspense>
|
||||||
|
<PaymentViaLicenseDialogContent
|
||||||
|
dialogName={dialogName}
|
||||||
|
subscriptionForm={payload}
|
||||||
|
/>
|
||||||
|
</DialogSuspense>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withDialogRedux(),
|
||||||
|
)(PaymentViaLicenseDialog);
|
||||||
@@ -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);
|
||||||
|
};
|
||||||
@@ -18,7 +18,7 @@ function SetupCongratsPage({
|
|||||||
|
|
||||||
const handleBtnClick = useCallback(() => {
|
const handleBtnClick = useCallback(() => {
|
||||||
setOrganizationSetupCompleted(false);
|
setOrganizationSetupCompleted(false);
|
||||||
history.push('/');
|
history.push('/homepage');
|
||||||
}, [
|
}, [
|
||||||
setOrganizationSetupCompleted,
|
setOrganizationSetupCompleted,
|
||||||
history,
|
history,
|
||||||
|
|||||||
13
client/src/containers/Setup/SetupDialogs.js
Normal file
13
client/src/containers/Setup/SetupDialogs.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PaymentViaVoucherDialog from 'containers/Dialogs/PaymentViaVoucherDialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup dialogs.
|
||||||
|
*/
|
||||||
|
export default function SetupDialogs() {
|
||||||
|
return (
|
||||||
|
<div class="setup-dialogs">
|
||||||
|
<PaymentViaVoucherDialog dialogName={'payment-via-voucher'} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ function SetupInitializingForm({
|
|||||||
return (
|
return (
|
||||||
<div class="setup-initializing-form">
|
<div class="setup-initializing-form">
|
||||||
<ProgressBar intent={Intent.PRIMARY} value={null} />
|
<ProgressBar intent={Intent.PRIMARY} value={null} />
|
||||||
|
|
||||||
<div className={'setup-initializing-form__title'}>
|
<div className={'setup-initializing-form__title'}>
|
||||||
<h1>
|
<h1>
|
||||||
{/* You organization is initializin... */}
|
{/* You organization is initializin... */}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Icon, For } from 'components';
|
import { Icon, For } from 'components';
|
||||||
import { FormattedMessage as T } from 'react-intl';
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
|
|
||||||
import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions';
|
import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions';
|
||||||
import withAuthentication from 'containers/Authentication/withAuthentication';
|
import withAuthentication from 'containers/Authentication/withAuthentication';
|
||||||
|
|
||||||
import footerLinks from 'config/footerLinks';
|
import footerLinks from 'config/footerLinks';
|
||||||
|
|
||||||
import { compose } from 'utils';
|
import { compose } from 'utils';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import React, { useMemo, useState, useCallback } from 'react';
|
import React from 'react';
|
||||||
import * as Yup from 'yup';
|
import { FastField, Form, ErrorMessage } from 'formik';
|
||||||
import { useFormik } from 'formik';
|
|
||||||
import { Row, Col } from 'react-grid-system';
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Intent,
|
Intent,
|
||||||
@@ -11,429 +9,218 @@ import {
|
|||||||
Classes,
|
Classes,
|
||||||
Position,
|
Position,
|
||||||
} from '@blueprintjs/core';
|
} from '@blueprintjs/core';
|
||||||
import moment from 'moment';
|
import { DateInput } from '@blueprintjs/datetime';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { TimezonePicker } from '@blueprintjs/timezone';
|
import { TimezonePicker } from '@blueprintjs/timezone';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
import { DateInput } from '@blueprintjs/datetime';
|
|
||||||
import { withWizard } from 'react-albus';
|
|
||||||
|
|
||||||
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 fiscalYearOptions from 'common/fiscalYearOptions';
|
||||||
import { ListSelect, ErrorMessage, FieldRequiredHint } from 'components';
|
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 }) => (
|
|
||||||
<MenuItem key={item.id} text={item.name} onClick={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 (
|
return (
|
||||||
<div className={'setup-organization'}>
|
<Form>
|
||||||
<div className={'setup-organization__title-wrap'}>
|
<h3>
|
||||||
<h1>
|
<T id={'organization_details'} />
|
||||||
<T id={'let_s_get_started'} />
|
</h3>
|
||||||
</h1>
|
|
||||||
<p class="paragraph">
|
|
||||||
<T id={'tell_the_system_a_little_bit_about_your_organization'} />
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form class="setup-organization__form" onSubmit={handleSubmit}>
|
{/* ---------- Organization name ---------- */}
|
||||||
<h3>
|
<FastField name={'name'}>
|
||||||
<T id={'organization_details'} />
|
{({ form, field, meta: { error, touched } }) => (
|
||||||
</h3>
|
<FormGroup
|
||||||
|
label={<T id={'legal_organization_name'} />}
|
||||||
<FormGroup
|
className={'form-group--name'}
|
||||||
label={<T id={'legal_organization_name'} />}
|
intent={inputIntent({ error, touched })}
|
||||||
labelInfo={<FieldRequiredHint />}
|
helperText={<ErrorMessage name={'organization_name'} />}
|
||||||
className={'form-group--name'}
|
|
||||||
intent={errors.name && touched.name && Intent.DANGER}
|
|
||||||
helperText={<ErrorMessage {...{ errors, touched }} name={'name'} />}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={errors.name && touched.name && Intent.DANGER}
|
|
||||||
{...getFieldProps('name')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{/* financial starting date */}
|
|
||||||
<FormGroup
|
|
||||||
label={<T id={'financial_starting_date'} />}
|
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
intent={
|
|
||||||
errors.financial_date_start &&
|
|
||||||
touched.financial_date_start &&
|
|
||||||
Intent.DANGER
|
|
||||||
}
|
|
||||||
helperText={
|
|
||||||
<ErrorMessage
|
|
||||||
name="financial_date_start"
|
|
||||||
{...{ errors, touched }}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
className={classNames('form-group--select-list', Classes.FILL)}
|
|
||||||
>
|
|
||||||
<DateInput
|
|
||||||
{...momentFormatter('MMMM Do YYYY')}
|
|
||||||
value={tansformDateValue(values.financial_date_start)}
|
|
||||||
onChange={handleDateChange}
|
|
||||||
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
<Row>
|
|
||||||
{/* base currency */}
|
|
||||||
<Col width={300}>
|
|
||||||
<FormGroup
|
|
||||||
label={<T id={'base_currency'} />}
|
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
className={classNames(
|
|
||||||
'form-group--base-currency',
|
|
||||||
'form-group--select-list',
|
|
||||||
Classes.LOADING,
|
|
||||||
Classes.FILL,
|
|
||||||
)}
|
|
||||||
intent={
|
|
||||||
errors.base_currency && touched.base_currency && Intent.DANGER
|
|
||||||
}
|
|
||||||
helperText={
|
|
||||||
<ErrorMessage name={'base_currency'} {...{ errors, touched }} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListSelect
|
|
||||||
items={baseCurrency}
|
|
||||||
noResults={<MenuItem disabled={true} text="No results." />}
|
|
||||||
itemRenderer={onItemRenderer}
|
|
||||||
popoverProps={{ minimal: true }}
|
|
||||||
onItemSelect={onItemsSelect('base_currency')}
|
|
||||||
itemPredicate={filterItems}
|
|
||||||
selectedItem={values.base_currency}
|
|
||||||
selectedItemProp={'value'}
|
|
||||||
defaultText={<T id={'select_base_currency'} />}
|
|
||||||
textProp={'name'}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Col>
|
|
||||||
|
|
||||||
{/* language */}
|
|
||||||
<Col width={300}>
|
|
||||||
<FormGroup
|
|
||||||
label={<T id={'language'} />}
|
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
className={classNames(
|
|
||||||
'form-group--language',
|
|
||||||
'form-group--select-list',
|
|
||||||
Classes.FILL,
|
|
||||||
)}
|
|
||||||
intent={errors.language && touched.language && Intent.DANGER}
|
|
||||||
helperText={
|
|
||||||
<ErrorMessage name={'language'} {...{ errors, touched }} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListSelect
|
|
||||||
items={languages}
|
|
||||||
noResults={<MenuItem disabled={true} text="No results." />}
|
|
||||||
itemRenderer={onItemRenderer}
|
|
||||||
popoverProps={{ minimal: true }}
|
|
||||||
onItemSelect={onItemsSelect('language')}
|
|
||||||
itemPredicate={filterItems}
|
|
||||||
selectedItem={values.language}
|
|
||||||
selectedItemProp={'value'}
|
|
||||||
defaultText={<T id={'select_language'} />}
|
|
||||||
textProp={'name'}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
{/* fiscal Year */}
|
|
||||||
<FormGroup
|
|
||||||
label={<T id={'fiscal_year'} />}
|
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
className={classNames(
|
|
||||||
'form-group--fiscal_year',
|
|
||||||
'form-group--select-list',
|
|
||||||
Classes.FILL,
|
|
||||||
)}
|
|
||||||
intent={errors.fiscal_year && touched.fiscal_year && Intent.DANGER}
|
|
||||||
helperText={
|
|
||||||
<ErrorMessage name={'fiscal_year'} {...{ errors, touched }} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ListSelect
|
|
||||||
items={fiscalYear}
|
|
||||||
noResults={<MenuItem disabled={true} text="No results." />}
|
|
||||||
itemRenderer={onItemRenderer}
|
|
||||||
popoverProps={{ minimal: true }}
|
|
||||||
onItemSelect={onItemsSelect('fiscal_year')}
|
|
||||||
itemPredicate={filterItems}
|
|
||||||
selectedItem={values.fiscal_year}
|
|
||||||
selectedItemProp={'value'}
|
|
||||||
defaultText={<T id={'select_fiscal_year'} />}
|
|
||||||
textProp={'name'}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
{/* Time zone */}
|
|
||||||
<FormGroup
|
|
||||||
label={<T id={'time_zone'} />}
|
|
||||||
labelInfo={<FieldRequiredHint />}
|
|
||||||
className={classNames(
|
|
||||||
'form-group--time-zone',
|
|
||||||
'form-group--select-list',
|
|
||||||
Classes.FILL,
|
|
||||||
)}
|
|
||||||
intent={errors.time_zone && touched.time_zone && Intent.DANGER}
|
|
||||||
helperText={
|
|
||||||
<ErrorMessage {...{ errors, touched }} name={'time_zone'} />
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TimezonePicker
|
|
||||||
value={values.time_zone}
|
|
||||||
onChange={handleTimeZoneChange}
|
|
||||||
valueDisplayFormat="composite"
|
|
||||||
showLocalTimezone={true}
|
|
||||||
placeholder={<T id={'select_time_zone'} />}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
|
|
||||||
<p className={'register-org-note'}>
|
|
||||||
<T
|
|
||||||
id={
|
|
||||||
'note_you_can_change_your_preferences_later_in_dashboard_if_needed'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<div className={'register-org-button'}>
|
|
||||||
<Button
|
|
||||||
intent={Intent.PRIMARY}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
loading={isSubmitting}
|
|
||||||
type="submit"
|
|
||||||
>
|
>
|
||||||
<T id={'save_continue'} />
|
<InputGroup {...field} />
|
||||||
</Button>
|
</FormGroup>
|
||||||
</div>
|
)}
|
||||||
</form>
|
</FastField>
|
||||||
</div>
|
|
||||||
|
{/* ---------- Financial starting date ---------- */}
|
||||||
|
<FastField name={'financialDateStart'}>
|
||||||
|
{({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => (
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'financial_starting_date'} />}
|
||||||
|
intent={inputIntent({ error, touched })}
|
||||||
|
helperText={<ErrorMessage name="financialDateStart" />}
|
||||||
|
className={classNames('form-group--select-list', Classes.FILL)}
|
||||||
|
>
|
||||||
|
<DateInput
|
||||||
|
{...momentFormatter('YYYY MMMM DD')}
|
||||||
|
value={tansformDateValue(value)}
|
||||||
|
popoverProps={{ position: Position.BOTTOM, minimal: true }}
|
||||||
|
onChange={handleDateChange((formattedDate) => {
|
||||||
|
setFieldValue('financialDateStart', formattedDate);
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</FastField>
|
||||||
|
|
||||||
|
<Row>
|
||||||
|
<Col xs={6}>
|
||||||
|
{/* ---------- Base currency ---------- */}
|
||||||
|
<FastField name={'baseCurrency'}>
|
||||||
|
{({
|
||||||
|
form: { setFieldValue },
|
||||||
|
field: { value },
|
||||||
|
meta: { error, touched },
|
||||||
|
}) => (
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'base_currency'} />}
|
||||||
|
className={classNames(
|
||||||
|
'form-group--base-currency',
|
||||||
|
'form-group--select-list',
|
||||||
|
Classes.FILL,
|
||||||
|
)}
|
||||||
|
intent={inputIntent({ error, touched })}
|
||||||
|
helperText={<ErrorMessage name={'baseCurrency'} />}
|
||||||
|
>
|
||||||
|
<ListSelect
|
||||||
|
items={currencies}
|
||||||
|
noResults={<MenuItem disabled={true} text="No results." />}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
onItemSelect={(item) => {
|
||||||
|
setFieldValue('baseCurrency', item.code);
|
||||||
|
}}
|
||||||
|
selectedItemProp={'code'}
|
||||||
|
textProp={'label'}
|
||||||
|
defaultText={<T id={'select_base_currency'} />}
|
||||||
|
selectedItem={value}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</FastField>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* ---------- Language ---------- */}
|
||||||
|
<Col xs={6}>
|
||||||
|
<FastField name={'language'}>
|
||||||
|
{({
|
||||||
|
form: { setFieldValue },
|
||||||
|
field: { value },
|
||||||
|
meta: { error, touched },
|
||||||
|
}) => (
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'language'} />}
|
||||||
|
className={classNames(
|
||||||
|
'form-group--language',
|
||||||
|
'form-group--select-list',
|
||||||
|
Classes.FILL,
|
||||||
|
)}
|
||||||
|
intent={inputIntent({ error, touched })}
|
||||||
|
helperText={<ErrorMessage name={'language'} />}
|
||||||
|
>
|
||||||
|
<ListSelect
|
||||||
|
items={languages}
|
||||||
|
noResults={<MenuItem disabled={true} text="No results." />}
|
||||||
|
onItemSelect={(item) => {
|
||||||
|
setFieldValue('language', item.value);
|
||||||
|
}}
|
||||||
|
selectedItem={value}
|
||||||
|
textProp={'name'}
|
||||||
|
selectedItemProp={'value'}
|
||||||
|
defaultText={<T id={'select_language'} />}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</FastField>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{/* --------- Fiscal Year ----------- */}
|
||||||
|
<FastField name={'fiscalYear'}>
|
||||||
|
{({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => (
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'fiscal_year'} />}
|
||||||
|
className={classNames(
|
||||||
|
'form-group--fiscal_year',
|
||||||
|
'form-group--select-list',
|
||||||
|
Classes.FILL,
|
||||||
|
)}
|
||||||
|
intent={inputIntent({ error, touched })}
|
||||||
|
helperText={<ErrorMessage name={'fiscalYear'} />}
|
||||||
|
>
|
||||||
|
<ListSelect
|
||||||
|
items={fiscalYearOptions}
|
||||||
|
noResults={<MenuItem disabled={true} text="No results." />}
|
||||||
|
selectedItem={value}
|
||||||
|
selectedItemProp={'value'}
|
||||||
|
textProp={'name'}
|
||||||
|
defaultText={<T id={'select_fiscal_year'} />}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
onItemSelect={(item) => {
|
||||||
|
setFieldValue('fiscalYear', item.value)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</FastField>
|
||||||
|
|
||||||
|
{/* ---------- Time zone ---------- */}
|
||||||
|
<FastField name={'timeZone'}>
|
||||||
|
{({
|
||||||
|
form: { setFieldValue },
|
||||||
|
field: { value },
|
||||||
|
meta: { error, touched },
|
||||||
|
}) => (
|
||||||
|
<FormGroup
|
||||||
|
label={<T id={'time_zone'} />}
|
||||||
|
className={classNames(
|
||||||
|
'form-group--time-zone',
|
||||||
|
'form-group--select-list',
|
||||||
|
Classes.FILL,
|
||||||
|
)}
|
||||||
|
intent={inputIntent({ error, touched })}
|
||||||
|
helperText={<ErrorMessage name={'timeZone'} />}
|
||||||
|
>
|
||||||
|
<TimezonePicker
|
||||||
|
value={value}
|
||||||
|
onChange={(item) => {
|
||||||
|
setFieldValue('timeZone', item);
|
||||||
|
}}
|
||||||
|
valueDisplayFormat="composite"
|
||||||
|
showLocalTimezone={true}
|
||||||
|
placeholder={<T id={'select_time_zone'} />}
|
||||||
|
popoverProps={{ minimal: true }}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</FastField>
|
||||||
|
|
||||||
|
<p className={'register-org-note'}>
|
||||||
|
<T
|
||||||
|
id={
|
||||||
|
'note_you_can_change_your_preferences_later_in_dashboard_if_needed'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={'register-org-button'}>
|
||||||
|
<Button
|
||||||
|
intent={Intent.PRIMARY}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
loading={isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<T id={'save_continue'} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(
|
|
||||||
withSettingsActions,
|
|
||||||
withOrganizationActions,
|
|
||||||
withWizard,
|
|
||||||
)(SetupOrganizationForm);
|
|
||||||
|
|||||||
139
client/src/containers/Setup/SetupOrganizationPage.js
Normal file
139
client/src/containers/Setup/SetupOrganizationPage.js
Normal file
@@ -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 (
|
||||||
|
<div className={'setup-organization'}>
|
||||||
|
<div className={'setup-organization__title-wrap'}>
|
||||||
|
<h1>
|
||||||
|
<T id={'let_s_get_started'} />
|
||||||
|
</h1>
|
||||||
|
<p class="paragraph">
|
||||||
|
<T id={'tell_the_system_a_little_bit_about_your_organization'} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Formik
|
||||||
|
validationSchema={validationSchema}
|
||||||
|
initialValues={initialValues}
|
||||||
|
component={SetupOrganizationForm}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withSettingsActions,
|
||||||
|
withOrganizationActions,
|
||||||
|
withWizard,
|
||||||
|
withSettings(({ organizationSettings }) => ({
|
||||||
|
organizationSettings,
|
||||||
|
})),
|
||||||
|
)(SetupOrganizationPage);
|
||||||
@@ -1,56 +1,52 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React from 'react';
|
||||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
|
||||||
import { Wizard, Steps, Step } from 'react-albus';
|
import { Wizard } from 'react-albus';
|
||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from 'react-router-dom';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import WizardSetupSteps from './WizardSetupSteps';
|
|
||||||
import withSubscriptions from 'containers/Subscriptions/withSubscriptions';
|
import withSubscriptions from 'containers/Subscriptions/withSubscriptions';
|
||||||
|
|
||||||
import SetupSubscriptionForm from './SetupSubscriptionForm';
|
import SetupDialogs from './SetupDialogs';
|
||||||
import SetupOrganizationForm from './SetupOrganizationForm';
|
import SetupWizardContent from './SetupWizardContent';
|
||||||
import SetupInitializingForm from './SetupInitializingForm';
|
|
||||||
import SetupCongratsPage from './SetupCongratsPage';
|
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';
|
import { compose } from 'utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wizard setup right section.
|
* Wizard setup right section.
|
||||||
*/
|
*/
|
||||||
function SetupRightSection ({
|
function SetupRightSection({
|
||||||
// #withAuthentication
|
|
||||||
currentOrganizationId,
|
|
||||||
|
|
||||||
// #withOrganization
|
// #withOrganization
|
||||||
isOrganizationInitialized,
|
isOrganizationInitialized,
|
||||||
isOrganizationSeeded,
|
isOrganizationSeeded,
|
||||||
isOrganizationSetupCompleted,
|
isOrganizationSetupCompleted,
|
||||||
|
|
||||||
|
// #withSetupWizard
|
||||||
|
isCongratsStep,
|
||||||
|
isSubscriptionStep,
|
||||||
|
isInitializingStep,
|
||||||
|
isOrganizationStep,
|
||||||
|
|
||||||
// #withSubscriptions
|
// #withSubscriptions
|
||||||
isSubscriptionActive
|
isSubscriptionActive,
|
||||||
}) {
|
}) {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const handleSkip = useCallback(({ step, push }) => {
|
const handleSkip = ({ step, push }) => {
|
||||||
const scenarios = [
|
const scenarios = [
|
||||||
{ condition: isOrganizationSetupCompleted, redirectTo: 'congrats' },
|
{ condition: isCongratsStep, redirectTo: 'congrats' },
|
||||||
{ condition: !isSubscriptionActive, redirectTo: 'subscription' },
|
{ condition: isSubscriptionStep, redirectTo: 'subscription' },
|
||||||
{ condition: isSubscriptionActive && !isOrganizationInitialized, redirectTo: 'initializing' },
|
{ condition: isInitializingStep, redirectTo: 'initializing' },
|
||||||
{ condition: isSubscriptionActive && !isOrganizationSeeded, redirectTo: 'organization' },
|
{ condition: isOrganizationStep, redirectTo: 'organization' },
|
||||||
];
|
];
|
||||||
const scenario = scenarios.find((scenario) => scenario.condition);
|
const scenario = scenarios.find((scenario) => scenario.condition);
|
||||||
|
|
||||||
if (scenario) {
|
if (scenario) {
|
||||||
push(scenario.redirectTo);
|
push(scenario.redirectTo);
|
||||||
}
|
}
|
||||||
}, [
|
};
|
||||||
isSubscriptionActive,
|
|
||||||
isOrganizationInitialized,
|
|
||||||
isOrganizationSeeded,
|
|
||||||
isOrganizationSetupCompleted
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={'setup-page__right-section'}>
|
<section className={'setup-page__right-section'}>
|
||||||
@@ -58,58 +54,47 @@ function SetupRightSection ({
|
|||||||
onNext={handleSkip}
|
onNext={handleSkip}
|
||||||
basename={'/setup'}
|
basename={'/setup'}
|
||||||
history={history}
|
history={history}
|
||||||
render={({ step, steps }) => (
|
render={SetupWizardContent}
|
||||||
<div class="setup-page__content">
|
/>
|
||||||
<WizardSetupSteps currentStep={steps.indexOf(step) + 1} />
|
<SetupDialogs />
|
||||||
|
|
||||||
<TransitionGroup>
|
|
||||||
<CSSTransition key={step.id} timeout={{ enter: 500, exit: 500 }}>
|
|
||||||
<div class="register-page-form">
|
|
||||||
<Steps key={step.id} step={step}>
|
|
||||||
<Step id="subscription">
|
|
||||||
<SetupSubscriptionForm />
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step id={'initializing'}>
|
|
||||||
<SetupInitializingForm />
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step id="organization">
|
|
||||||
<SetupOrganizationForm />
|
|
||||||
</Step>
|
|
||||||
|
|
||||||
<Step id="congrats">
|
|
||||||
<SetupCongratsPage />
|
|
||||||
</Step>
|
|
||||||
</Steps>
|
|
||||||
</div>
|
|
||||||
</CSSTransition>
|
|
||||||
</TransitionGroup>
|
|
||||||
</div>
|
|
||||||
)} />
|
|
||||||
</section>
|
</section>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(
|
export default compose(
|
||||||
withAuthentication(({ currentOrganizationId }) => ({ currentOrganizationId })),
|
withCurrentOrganization(({ organizationTenantId }) => ({
|
||||||
connect((state, props) => ({
|
organizationId: organizationTenantId,
|
||||||
organizationId: props.currentOrganizationId,
|
|
||||||
})),
|
})),
|
||||||
withOrganization(({
|
withOrganization(
|
||||||
organization,
|
({
|
||||||
isOrganizationInitialized,
|
organization,
|
||||||
isOrganizationSeeded,
|
isOrganizationInitialized,
|
||||||
isOrganizationSetupCompleted
|
isOrganizationSeeded,
|
||||||
}) => ({
|
isOrganizationSetupCompleted,
|
||||||
organization,
|
}) => ({
|
||||||
isOrganizationInitialized,
|
organization,
|
||||||
isOrganizationSeeded,
|
isOrganizationInitialized,
|
||||||
isOrganizationSetupCompleted
|
isOrganizationSeeded,
|
||||||
})),
|
isOrganizationSetupCompleted,
|
||||||
withSubscriptions(({
|
}),
|
||||||
isSubscriptionActive,
|
),
|
||||||
}) => ({
|
withSubscriptions(
|
||||||
isSubscriptionActive
|
({ isSubscriptionActive }) => ({
|
||||||
}), 'main'),
|
isSubscriptionActive,
|
||||||
|
}),
|
||||||
|
'main',
|
||||||
|
),
|
||||||
|
withSetupWizard(
|
||||||
|
({
|
||||||
|
isCongratsStep,
|
||||||
|
isSubscriptionStep,
|
||||||
|
isInitializingStep,
|
||||||
|
isOrganizationStep,
|
||||||
|
}) => ({
|
||||||
|
isCongratsStep,
|
||||||
|
isSubscriptionStep,
|
||||||
|
isInitializingStep,
|
||||||
|
isOrganizationStep,
|
||||||
|
}),
|
||||||
|
),
|
||||||
)(SetupRightSection);
|
)(SetupRightSection);
|
||||||
42
client/src/containers/Setup/SetupSubscription.js
Normal file
42
client/src/containers/Setup/SetupSubscription.js
Normal file
@@ -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 (
|
||||||
|
<div className={'setup-subscription-form'}>
|
||||||
|
<Formik
|
||||||
|
validationSchema={SubscriptionFormSchema}
|
||||||
|
initialValues={initialValues}
|
||||||
|
component={SetupSubscriptionForm}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withWizard,
|
||||||
|
)(SetupSubscription);
|
||||||
@@ -1,98 +1,15 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React from 'react';
|
||||||
import * as Yup from 'yup';
|
import { Form } from 'formik';
|
||||||
import { useFormik } from 'formik';
|
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
|
||||||
import { Button, Intent } from '@blueprintjs/core';
|
|
||||||
import { withWizard } from 'react-albus';
|
|
||||||
|
|
||||||
import 'style/pages/Setup/Billing.scss';
|
import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm';
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription step of wizard setup.
|
* Subscription step of wizard setup.
|
||||||
*/
|
*/
|
||||||
function SetupSubscriptionForm({
|
export default 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);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div className={'register-subscription-form'}>
|
<Form>
|
||||||
<form onSubmit={formik.handleSubmit} className={'billing-form'}>
|
<BillingPlansForm />
|
||||||
<BillingPlans title={'a_select_a_plan'} formik={formik} />
|
</Form>
|
||||||
<BillingPeriods title={'b_choose_your_billing'} formik={formik} />
|
|
||||||
<BillingPaymentmethod title={'c_payment_methods'} formik={formik} />
|
|
||||||
|
|
||||||
<div className={'subscribe-button'}>
|
|
||||||
<Button
|
|
||||||
intent={Intent.PRIMARY}
|
|
||||||
type="submit"
|
|
||||||
loading={formik.isSubmitting}
|
|
||||||
>
|
|
||||||
<T id={'subscribe'} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default compose(
|
|
||||||
withBillingActions,
|
|
||||||
withWizard,
|
|
||||||
withSubscriptionsActions,
|
|
||||||
)(SetupSubscriptionForm);
|
|
||||||
|
|||||||
48
client/src/containers/Setup/SetupWizardContent.js
Normal file
48
client/src/containers/Setup/SetupWizardContent.js
Normal file
@@ -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 (
|
||||||
|
<div class="setup-page__content">
|
||||||
|
<WizardSetupSteps currentStep={steps.indexOf(step) + 1} />
|
||||||
|
|
||||||
|
<TransitionGroup>
|
||||||
|
<CSSTransition key={step.id} timeout={{ enter: 500, exit: 500 }}>
|
||||||
|
<div class="setup-page-form">
|
||||||
|
<Steps key={step.id} step={step}>
|
||||||
|
<Step id="subscription">
|
||||||
|
<SetupSubscription />
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step id={'initializing'}>
|
||||||
|
<SetupInitializingForm />
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step id="organization">
|
||||||
|
<SetupOrganizationPage />
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step id="congrats">
|
||||||
|
<SetupCongratsPage />
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
</div>
|
||||||
|
</CSSTransition>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
client/src/containers/Setup/SubscriptionForm.schema.js
Normal file
9
client/src/containers/Setup/SubscriptionForm.schema.js
Normal file
@@ -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(),
|
||||||
|
});
|
||||||
@@ -1,13 +1,21 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import { useFormik } from 'formik';
|
import { Formik, Form } from 'formik';
|
||||||
import { Button, Intent } from '@blueprintjs/core';
|
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
import DashboardInsider from 'components/Dashboard/DashboardInsider';
|
||||||
import { MeteredBillingTabs, PaymentMethodTabs } from './SubscriptionTabs';
|
|
||||||
|
import 'style/pages/Billing/BillingPage.scss';
|
||||||
|
|
||||||
|
import { MasterBillingTabs } from './SubscriptionTabs';
|
||||||
|
|
||||||
import withBillingActions from './withBillingActions';
|
import withBillingActions from './withBillingActions';
|
||||||
|
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||||
|
|
||||||
import { compose } from 'utils';
|
import { compose } from 'utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Billing form.
|
||||||
|
*/
|
||||||
function BillingForm({
|
function BillingForm({
|
||||||
// #withDashboardActions
|
// #withDashboardActions
|
||||||
changePageTitle,
|
changePageTitle,
|
||||||
@@ -23,51 +31,43 @@ function BillingForm({
|
|||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
plan_slug: Yup.string()
|
plan_slug: Yup.string()
|
||||||
.required()
|
.required(),
|
||||||
.label(formatMessage({ id: 'plan_slug' })),
|
period: Yup.string().required(),
|
||||||
license_code: Yup.string().trim(),
|
license_code: Yup.string().trim(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = useMemo(
|
const initialValues = {
|
||||||
() => ({
|
plan_slug: 'free',
|
||||||
plan_slug: 'free',
|
period: 'month',
|
||||||
license_code: '',
|
license_code: '',
|
||||||
}),
|
};
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
const formik = useFormik({
|
const handleSubmit = (values, { setSubmitting }) => {
|
||||||
enableReinitialize: true,
|
requestSubmitBilling(values)
|
||||||
validationSchema: validationSchema,
|
.then((response) => {
|
||||||
initialValues: {
|
setSubmitting(false);
|
||||||
...initialValues,
|
})
|
||||||
},
|
.catch((errors) => {
|
||||||
onSubmit: (values, { setSubmitting, resetForm, setErrors }) => {
|
setSubmitting(false);
|
||||||
requestSubmitBilling(values)
|
});
|
||||||
.then((response) => {
|
};
|
||||||
setSubmitting(false);
|
|
||||||
})
|
|
||||||
.catch((errors) => {
|
|
||||||
setSubmitting(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'billing-form'}>
|
<DashboardInsider name={'billing-page'}>
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<div className={'billing-page'}>
|
||||||
<MeteredBillingTabs formik={formik} planId={formik.values.plan_slug} />
|
<Formik
|
||||||
<div className={'subscribe-button'}>
|
validationSchema={validationSchema}
|
||||||
<Button
|
onSubmit={handleSubmit}
|
||||||
intent={Intent.PRIMARY}
|
initialValues={initialValues}
|
||||||
type="submit"
|
>
|
||||||
loading={formik.isSubmitting}
|
{({ isSubmitting, handleSubmit }) => (
|
||||||
>
|
<Form>
|
||||||
<T id={'subscribe'} />
|
<MasterBillingTabs />
|
||||||
</Button>
|
</Form>
|
||||||
</div>
|
)}
|
||||||
</form>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
|
</DashboardInsider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
56
client/src/containers/Subscriptions/BillingPeriod.js
Normal file
56
client/src/containers/Subscriptions/BillingPeriod.js
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
id={`plan-period-${period}`}
|
||||||
|
className={classNames(
|
||||||
|
{
|
||||||
|
'is-selected': value === selectedOption,
|
||||||
|
},
|
||||||
|
'period-radio',
|
||||||
|
)}
|
||||||
|
onClick={handlePeriodClick}
|
||||||
|
>
|
||||||
|
<span className={'period-radio__label'}>{label}</span>
|
||||||
|
|
||||||
|
<div className={'period-radio__price'}>
|
||||||
|
<span className={'period-radio__amount'}>
|
||||||
|
{price} {currencyCode}
|
||||||
|
</span>
|
||||||
|
<span className={'period-radio__period'}>{label}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(
|
||||||
|
withPlan(({ plan }, state, { period }) => ({
|
||||||
|
price: get(plan, `price.${period}`),
|
||||||
|
currencyCode: get(plan, 'currencyCode'),
|
||||||
|
})),
|
||||||
|
)(BillingPeriod);
|
||||||
42
client/src/containers/Subscriptions/BillingPeriodsInput.js
Normal file
42
client/src/containers/Subscriptions/BillingPeriodsInput.js
Normal file
@@ -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 (
|
||||||
|
<section class="billing-plans__section">
|
||||||
|
<h1 class="title">{title}</h1>
|
||||||
|
<div class="description">
|
||||||
|
<p className="paragraph">{description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Field name={'period'}>
|
||||||
|
{({ form: { setFieldValue, values } }) => (
|
||||||
|
<div className={'plan-periods'}>
|
||||||
|
{plansPeriods.map((period) => (
|
||||||
|
<BillingPeriod
|
||||||
|
planSlug={values.plan_slug}
|
||||||
|
period={period.slug}
|
||||||
|
label={period.label}
|
||||||
|
value={period.slug}
|
||||||
|
onSelected={(value) => setFieldValue('period', value)}
|
||||||
|
selectedOption={values.period}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default compose(withPlans(({ plansPeriods }) => ({ plansPeriods })))(
|
||||||
|
BillingPeriods,
|
||||||
|
);
|
||||||
57
client/src/containers/Subscriptions/BillingPlan.js
Normal file
57
client/src/containers/Subscriptions/BillingPlan.js
Normal file
@@ -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 (
|
||||||
|
<div
|
||||||
|
id={'basic-plan'}
|
||||||
|
className={classNames('plan-radio', {
|
||||||
|
'is-selected': selectedOption === value,
|
||||||
|
})}
|
||||||
|
onClick={handlePlanClick}
|
||||||
|
>
|
||||||
|
<div className={'plan-radio__header'}>
|
||||||
|
<div className={'plan-radio__name'}>
|
||||||
|
<T id={name} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'plan-radio__description'}>
|
||||||
|
<ul>
|
||||||
|
{description.map((line) => (
|
||||||
|
<li>{line}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={'plan-radio__price'}>
|
||||||
|
<span className={'plan-radio__amount'}>
|
||||||
|
{price} {currencyCode}
|
||||||
|
</span>
|
||||||
|
<span className={'plan-radio__period'}>
|
||||||
|
<T id={'monthly'} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
client/src/containers/Subscriptions/BillingPlansForm.js
Normal file
30
client/src/containers/Subscriptions/BillingPlansForm.js
Normal file
@@ -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 (
|
||||||
|
<div class="billing-plans">
|
||||||
|
<BillingPlansInput
|
||||||
|
title={<T id={'select_a_plan'} values={{ order: 1 }} />}
|
||||||
|
description={<T id={'please_enter_your_preferred_payment_method'} />}
|
||||||
|
/>
|
||||||
|
<BillingPeriodsInput
|
||||||
|
title={<T id={'choose_your_billing'} values={{ order: 2 }} />}
|
||||||
|
description={<T id={'please_enter_your_preferred_payment_method'} />}
|
||||||
|
/>
|
||||||
|
<BillingPaymentMethod
|
||||||
|
title={<T id={'payment_methods'} values={{ order: 3 }} />}
|
||||||
|
description={<T id={'please_enter_your_preferred_payment_method'} />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
client/src/containers/Subscriptions/BillingPlansInput.js
Normal file
40
client/src/containers/Subscriptions/BillingPlansInput.js
Normal file
@@ -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 (
|
||||||
|
<section class="billing-plans__section">
|
||||||
|
<h1 class="title">{title}</h1>
|
||||||
|
<div class="description">
|
||||||
|
<p className="paragraph">{description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FastField name={'plan_slug'}>
|
||||||
|
{({ form: { setFieldValue }, field: { value } }) => (
|
||||||
|
<div className={'plan-radios'}>
|
||||||
|
{plans.map((plan) => (
|
||||||
|
<BillingPlan
|
||||||
|
name={plan.name}
|
||||||
|
description={plan.description}
|
||||||
|
slug={plan.slug}
|
||||||
|
price={plan.price.month}
|
||||||
|
currencyCode={plan.currencyCode}
|
||||||
|
value={plan.slug}
|
||||||
|
onSelected={(value) => setFieldValue('plan_slug', value)}
|
||||||
|
selectedOption={value}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FastField>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export default compose(withPlans(({ plans }) => ({ plans })))(BillingPlans);
|
||||||
@@ -1,16 +1,10 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import BillingPlans from 'containers/Subscriptions/billingPlans';
|
import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm';
|
||||||
import BillingPeriods from 'containers/Subscriptions/billingPeriods';
|
|
||||||
import { BillingPaymentmethod } from 'containers/Subscriptions/billingPaymentmethod';
|
|
||||||
|
|
||||||
function BillingTab({ formik }) {
|
export default function BillingTab() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<BillingPlans title={'a_select_a_plan'} formik={formik} />
|
<BillingPlansForm />
|
||||||
<BillingPeriods title={'b_choose_your_billing'} formik={formik} />
|
|
||||||
<BillingPaymentmethod title={'c_payment_methods'} formik={formik} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BillingTab;
|
|
||||||
|
|||||||
@@ -1,35 +1,38 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { FormattedMessage as T } from 'react-intl';
|
||||||
import { InputGroup, FormGroup, Intent } from '@blueprintjs/core';
|
|
||||||
import ErrorMessage from 'components/ErrorMessage';
|
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 (
|
return (
|
||||||
<div className={'license-container'}>
|
<div className={'license-container'}>
|
||||||
<h4>
|
<h3>
|
||||||
<T id={'license_code'} />
|
<T id={'voucher'} />
|
||||||
</h4>
|
</h3>
|
||||||
<p className="paragraph">
|
<p className="paragraph">
|
||||||
<T id={'cards_will_be_charged'} />
|
<T id={'cards_will_be_charged'} />
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<FormGroup
|
<Button onClick={handleSubmitBtnClick} intent={Intent.PRIMARY} large={true}>
|
||||||
label={<T id={'license_number'} />}
|
Submit Voucher
|
||||||
intent={errors.license_code && touched.license_code && Intent.DANGER}
|
</Button>
|
||||||
helperText={
|
|
||||||
<ErrorMessage name="license_code" {...{ errors, touched }} />
|
|
||||||
}
|
|
||||||
className={'form-group-license_code'}
|
|
||||||
>
|
|
||||||
<InputGroup
|
|
||||||
intent={errors.license_code && touched.license_code && Intent.DANGER}
|
|
||||||
{...getFieldProps('license_code')}
|
|
||||||
/>
|
|
||||||
</FormGroup>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LicenseTab;
|
export default compose(withDialogActions)(LicenseTab);
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import { Tabs, Tab } from '@blueprintjs/core';
|
import { Tabs, Tab } from '@blueprintjs/core';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import BillingTab from './BillingTab';
|
import BillingTab from './BillingTab';
|
||||||
import LicenseTab from './LicenseTab';
|
import LicenseTab from './LicenseTab';
|
||||||
|
|
||||||
export const MeteredBillingTabs = ({ formik }) => {
|
/**
|
||||||
const [animate, setAnimate] = useState(true);
|
* Master billing tabs.
|
||||||
|
*/
|
||||||
|
export const MasterBillingTabs = ({ formik }) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tabs animate={animate} large={true}>
|
<Tabs animate={true} large={true}>
|
||||||
<Tab
|
<Tab
|
||||||
title={formatMessage({ id: 'billing' })}
|
title={formatMessage({ id: 'billing' })}
|
||||||
id={'billing'}
|
id={'billing'}
|
||||||
@@ -20,23 +22,24 @@ export const MeteredBillingTabs = ({ formik }) => {
|
|||||||
title={formatMessage({ id: 'usage' })}
|
title={formatMessage({ id: 'usage' })}
|
||||||
id={'usage'}
|
id={'usage'}
|
||||||
disabled={true}
|
disabled={true}
|
||||||
// panel={'Usage'}
|
|
||||||
/>
|
/>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payment methods tabs.
|
||||||
|
*/
|
||||||
export const PaymentMethodTabs = ({ formik }) => {
|
export const PaymentMethodTabs = ({ formik }) => {
|
||||||
const [animate, setAnimate] = useState(true);
|
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tabs animate={animate} large={true}>
|
<Tabs animate={true} large={true}>
|
||||||
<Tab
|
<Tab
|
||||||
title={formatMessage({ id: 'license' })}
|
title={formatMessage({ id: 'voucher' })}
|
||||||
id={'license'}
|
id={'voucher'}
|
||||||
panel={<LicenseTab formik={formik} />}
|
panel={<LicenseTab formik={formik} />}
|
||||||
/>
|
/>
|
||||||
<Tab
|
<Tab
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
|
||||||
import { PaymentMethodTabs } from './SubscriptionTabs';
|
import { PaymentMethodTabs } from './SubscriptionTabs';
|
||||||
|
|
||||||
export const BillingPaymentmethod = ({ formik, title }) => {
|
export default ({ formik, title, description }) => {
|
||||||
return (
|
return (
|
||||||
<section class="billing-section">
|
<section class="billing-plans__section">
|
||||||
<h1 className={'bg-title'}>
|
<h1 className="title">{ title }</h1>
|
||||||
<T id={title} />
|
<p className="paragraph">{ description }</p>
|
||||||
</h1>
|
|
||||||
<p className='paragraph'>
|
|
||||||
<T id={'please_enter_your_preferred_payment_method'} />
|
|
||||||
</p>
|
|
||||||
<PaymentMethodTabs formik={formik} />
|
<PaymentMethodTabs formik={formik} />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<a
|
|
||||||
href={'#!'}
|
|
||||||
id={'monthly'}
|
|
||||||
className={`period-container ${classNames({
|
|
||||||
'billing-selected': selected,
|
|
||||||
})} `}
|
|
||||||
>
|
|
||||||
<span className={'bg-period'}>
|
|
||||||
<T id={period} />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<div className={'plan-price'}>
|
|
||||||
<span className={'amount'}>
|
|
||||||
{price} {currency}
|
|
||||||
</span>
|
|
||||||
<span className={'period'}>
|
|
||||||
<T id={'year'} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
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 (
|
|
||||||
<section class="billing-section">
|
|
||||||
<h1>
|
|
||||||
<T id={title} />
|
|
||||||
</h1>
|
|
||||||
<p className='paragraph'>
|
|
||||||
<T id={'please_enter_your_preferred_payment_method'} />
|
|
||||||
</p>
|
|
||||||
<div className={'payment-method-continer'} ref={billingRef}>
|
|
||||||
{paymentmethod.map((pay, index) => (
|
|
||||||
<BillingPeriod
|
|
||||||
period={pay.period}
|
|
||||||
price={pay.price}
|
|
||||||
currency={pay.currency}
|
|
||||||
onSelected={()=>formik.setFieldValue('period', pay.period)}
|
|
||||||
selected={selected == index + 1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BillingPeriods;
|
|
||||||
@@ -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 (
|
|
||||||
<a
|
|
||||||
id={'basic-plan'}
|
|
||||||
className={`plan-wrapper ${classNames({
|
|
||||||
'plan-selected': selected,
|
|
||||||
})} `}
|
|
||||||
onClick={() => onSelected(slug)}
|
|
||||||
>
|
|
||||||
<div className={'plan-header'}>
|
|
||||||
<div className={'plan-name'}>
|
|
||||||
<T id={name} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className={'plan-description'}>
|
|
||||||
<ul>
|
|
||||||
{description.map((desc, index) => (
|
|
||||||
<li>{desc}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div className={'plan-price'}>
|
|
||||||
<span className={'amount'}>
|
|
||||||
{' '}
|
|
||||||
{price} {currency}
|
|
||||||
</span>
|
|
||||||
<span className={'period'}>
|
|
||||||
<T id={'year_per'} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<section class="billing-section">
|
|
||||||
<h1>
|
|
||||||
<T id={title} />
|
|
||||||
</h1>
|
|
||||||
<p className='paragraph'>
|
|
||||||
<T id={'please_enter_your_preferred_payment_method'} />
|
|
||||||
</p>
|
|
||||||
<div className={'billing-form__plan-container'} ref={planRef}>
|
|
||||||
{plans.map((plan, index) => (
|
|
||||||
<BillingPlan
|
|
||||||
name={plan.name}
|
|
||||||
description={plan.description}
|
|
||||||
slug={plan.slug}
|
|
||||||
price={plan.price}
|
|
||||||
currency={plan.currency}
|
|
||||||
onSelected={() => formik.setFieldValue('plan_slug', plan.slug)}
|
|
||||||
selected={selected == index + 1}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default BillingPlans;
|
|
||||||
|
|
||||||
17
client/src/containers/Subscriptions/withPlan.js
Normal file
17
client/src/containers/Subscriptions/withPlan.js
Normal file
@@ -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);
|
||||||
|
};
|
||||||
19
client/src/containers/Subscriptions/withPlans.js
Normal file
19
client/src/containers/Subscriptions/withPlans.js
Normal file
@@ -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);
|
||||||
|
};
|
||||||
@@ -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_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?`,
|
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_business_location: 'Select Business Location',
|
||||||
select_base_currency: 'Select Base Currency',
|
select_base_currency: 'Select base currency',
|
||||||
select_fiscal_year: 'Select Fiscal Year',
|
select_fiscal_year: 'Select fiscal year',
|
||||||
select_language: 'Select Language',
|
select_language: 'Select Language',
|
||||||
select_date_format: 'Select Date Format',
|
select_date_format: 'Select Date Format',
|
||||||
select_time_zone: 'Select Time Zone',
|
select_time_zone: 'Select Time Zone',
|
||||||
@@ -700,9 +700,9 @@ export default {
|
|||||||
billing: 'Billing',
|
billing: 'Billing',
|
||||||
the_billing_has_been_created_successfully:
|
the_billing_has_been_created_successfully:
|
||||||
'The billing has been created successfully.',
|
'The billing has been created successfully.',
|
||||||
a_select_a_plan: 'A. Select a plan',
|
select_a_plan: '{order}. Select a plan',
|
||||||
b_choose_your_billing: 'B. Choose your billing',
|
choose_your_billing: '{order}. Choose your billing',
|
||||||
c_payment_methods: 'C. Payment methods',
|
payment_methods: '{order}. Payment methods',
|
||||||
usage: 'Usage',
|
usage: 'Usage',
|
||||||
basic: 'Basic',
|
basic: 'Basic',
|
||||||
license: 'License',
|
license: 'License',
|
||||||
@@ -964,4 +964,7 @@ export default {
|
|||||||
selected_customers: '{count} Selected Customers',
|
selected_customers: '{count} Selected Customers',
|
||||||
transaction_number: 'Transaction #',
|
transaction_number: 'Transaction #',
|
||||||
running_balance: 'Running balance',
|
running_balance: 'Running balance',
|
||||||
|
payment_via_voucher: 'Payment via voucher',
|
||||||
|
voucher_number: 'Voucher number',
|
||||||
|
voucher: 'Voucher'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -404,5 +404,5 @@ export default [
|
|||||||
loader: () => import('containers/Purchases/PaymentMades/PaymentMadeList'),
|
loader: () => import('containers/Purchases/PaymentMades/PaymentMadeList'),
|
||||||
}),
|
}),
|
||||||
breadcrumb: 'Payment Made List',
|
breadcrumb: 'Payment Made List',
|
||||||
},
|
}
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ export const seedTenant = () => (dispatch, getState) => new Promise((resolve, re
|
|||||||
payload: { organizationId }
|
payload: { organizationId }
|
||||||
});
|
});
|
||||||
ApiService.post(`organization/seed/`).then((response) => {
|
ApiService.post(`organization/seed/`).then((response) => {
|
||||||
resolve(response);
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: t.SET_ORGANIZATION_SEEDED,
|
type: t.SET_ORGANIZATION_SEEDED,
|
||||||
payload: { organizationId }
|
payload: { organizationId }
|
||||||
});
|
});
|
||||||
|
resolve(response);
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
reject(error.response.data.errors || []);
|
reject(error.response.data.errors || []);
|
||||||
|
|||||||
@@ -54,4 +54,4 @@ export const isOrganizationCongratsFactory = () => createSelector(
|
|||||||
(organization) => {
|
(organization) => {
|
||||||
return !!organization?.is_congrats;
|
return !!organization?.is_congrats;
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|||||||
22
client/src/store/organizations/withSetupWizard.js
Normal file
22
client/src/store/organizations/withSetupWizard.js
Normal file
@@ -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);
|
||||||
|
};
|
||||||
52
client/src/store/plans/plans.reducer.js
Normal file
52
client/src/store/plans/plans.reducer.js
Normal file
@@ -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, {
|
||||||
|
|
||||||
|
});
|
||||||
29
client/src/store/plans/plans.selectors.js
Normal file
29
client/src/store/plans/plans.selectors.js
Normal file
@@ -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,
|
||||||
|
)
|
||||||
@@ -28,6 +28,7 @@ import paymentMades from './PaymentMades/paymentMade.reducer';
|
|||||||
import organizations from './organizations/organizations.reducers';
|
import organizations from './organizations/organizations.reducers';
|
||||||
import subscriptions from './subscription/subscription.reducer';
|
import subscriptions from './subscription/subscription.reducer';
|
||||||
import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer';
|
import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer';
|
||||||
|
import plans from './plans/plans.reducer';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
authentication,
|
authentication,
|
||||||
@@ -58,4 +59,5 @@ export default combineReducers({
|
|||||||
paymentReceives,
|
paymentReceives,
|
||||||
paymentMades,
|
paymentMades,
|
||||||
inventoryAdjustments,
|
inventoryAdjustments,
|
||||||
|
plans
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -96,3 +96,13 @@ body.hide-scrollbar .Pane2{
|
|||||||
justify-content: start;
|
justify-content: start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bp3-timezone-picker{
|
||||||
|
|
||||||
|
.bp3-button{
|
||||||
|
|
||||||
|
[icon="caret-down"] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
.#{$ns}-menu-item {
|
.#{$ns}-menu-item {
|
||||||
color: $sidebar-menu-item-color;
|
color: $sidebar-menu-item-color;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
padding: 8px 18px;
|
padding: 8px 20px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
display: block;
|
display: block;
|
||||||
color: $sidebar-menu-label-color;
|
color: $sidebar-menu-label-color;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
padding: 10px 18px;
|
padding: 10px 20px;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
.#{$ns}-menu-item {
|
.#{$ns}-menu-item {
|
||||||
padding: 8px 16px;
|
padding: 8px 20px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
color: $sidebar-submenu-item-color;
|
color: $sidebar-submenu-item-color;
|
||||||
|
|
||||||
|
|||||||
26
client/src/style/pages/Billing/BillingPage.scss
Normal file
26
client/src/style/pages/Billing/BillingPage.scss
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
padding: 45px 0 20px;
|
padding: 45px 0 20px;
|
||||||
|
|
||||||
&__title-wrap {
|
&__title-wrap {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
@@ -17,17 +17,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__form {
|
form {
|
||||||
h3 {
|
h3 {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bp3-form-group {
|
.bp3-form-group {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
.bp3-input-group {
|
.bp3-input-group {
|
||||||
.bp3-input {
|
.bp3-input {
|
||||||
height: 38px;
|
height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.bp3-input,
|
||||||
|
.form-group--select-list .bp3-button{
|
||||||
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
23
client/src/style/pages/Setup/PaymentViaVoucherDialog.scss
Normal file
23
client/src/style/pages/Setup/PaymentViaVoucherDialog.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
|
|
||||||
&__title {
|
&__title {
|
||||||
font-size: 26px;
|
font-size: 26px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
line-height: normal;
|
line-height: normal;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
margin-top: 14px;
|
margin-top: 14px;
|
||||||
@@ -131,11 +131,9 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 50px 0 0;
|
padding: 50px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
position: relative;
|
position: relative;
|
||||||
list-style-type: none;
|
list-style-type: none;
|
||||||
@@ -155,7 +153,6 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #75859c;
|
background-color: #75859c;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 2px;
|
height: 2px;
|
||||||
@@ -166,23 +163,19 @@
|
|||||||
left: -50%;
|
left: -50%;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child::after {
|
&:first-child::after {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.is-active {
|
&.is-active {
|
||||||
&::before {
|
&::before {
|
||||||
background-color: #75859c;
|
background-color: #75859c;
|
||||||
}
|
}
|
||||||
|
|
||||||
~ li {
|
~ li {
|
||||||
&:before,
|
&:before,
|
||||||
&:after {
|
&:after {
|
||||||
background: #ebebeb;
|
background: #ebebeb;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p.wizard-info {
|
p.wizard-info {
|
||||||
color: #004dd0;
|
color: #004dd0;
|
||||||
}
|
}
|
||||||
|
|||||||
7
client/src/style/pages/Setup/Subscription.scss
Normal file
7
client/src/style/pages/Setup/Subscription.scss
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
.setup-subscription-form{
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 60px;
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
69
client/src/style/pages/Subscription/BillingPlans.scss
Normal file
69
client/src/style/pages/Subscription/BillingPlans.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
43
client/src/style/pages/Subscription/PlanPeriodRadio.scss
Normal file
43
client/src/style/pages/Subscription/PlanPeriodRadio.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
client/src/style/pages/Subscription/PlanRadio.scss
Normal file
71
client/src/style/pages/Subscription/PlanRadio.scss
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,6 @@ $button-background-color-disabled: #E9ECEF !default;
|
|||||||
$button-background-color: #E6EFFB !default;
|
$button-background-color: #E6EFFB !default;
|
||||||
$button-background-color-hover: #CFDCEE !default;
|
$button-background-color-hover: #CFDCEE !default;
|
||||||
|
|
||||||
|
|
||||||
$sidebar-background: #01115e;
|
$sidebar-background: #01115e;
|
||||||
$sidebar-text-color: #fff;
|
$sidebar-text-color: #fff;
|
||||||
$sidebar-width: 100%;
|
$sidebar-width: 100%;
|
||||||
|
|||||||
@@ -407,6 +407,9 @@ export const transformToCamelCase = (object) => {
|
|||||||
return deepMapKeys(object, (key) => _.snakeCase(key));
|
return deepMapKeys(object, (key) => _.snakeCase(key));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const transfromToSnakeCase = (object) => {
|
||||||
|
return deepMapKeys(object, (key) => _.snakeCase(key));
|
||||||
|
};
|
||||||
|
|
||||||
export function flatObject(obj) {
|
export function flatObject(obj) {
|
||||||
const flatObject = {};
|
const flatObject = {};
|
||||||
@@ -428,3 +431,4 @@ export function flatObject(obj) {
|
|||||||
dig(obj);
|
dig(obj);
|
||||||
return flatObject;
|
return flatObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export default class SettingsController extends BaseController{
|
|||||||
* @param {Request} req -
|
* @param {Request} req -
|
||||||
* @param {Response} res -
|
* @param {Response} res -
|
||||||
*/
|
*/
|
||||||
saveSettings(req: Request, res: Response) {
|
async saveSettings(req: Request, res: Response) {
|
||||||
const { Option } = req.models;
|
const { Option } = req.models;
|
||||||
const optionsDTO: IOptionsDTO = this.matchedBodyData(req);
|
const optionsDTO: IOptionsDTO = this.matchedBodyData(req);
|
||||||
const { settings } = req;
|
const { settings } = req;
|
||||||
@@ -102,6 +102,8 @@ export default class SettingsController extends BaseController{
|
|||||||
});
|
});
|
||||||
this.observeAppConfigsComplete(settings);
|
this.observeAppConfigsComplete(settings);
|
||||||
|
|
||||||
|
await settings.save();
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
code: 'OPTIONS.SAVED.SUCCESSFULLY',
|
code: 'OPTIONS.SAVED.SUCCESSFULLY',
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import SettingsStore from 'services/Settings/SettingsStore';
|
|||||||
|
|
||||||
export default async (req: Request, res: Response, next: NextFunction) => {
|
export default async (req: Request, res: Response, next: NextFunction) => {
|
||||||
const { tenantId } = req.user;
|
const { tenantId } = req.user;
|
||||||
const { knex } = req;
|
|
||||||
|
|
||||||
const Logger = Container.get('logger');
|
const Logger = Container.get('logger');
|
||||||
const tenantContainer = Container.of(`tenant-${tenantId}`);
|
const tenantContainer = Container.of(`tenant-${tenantId}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user