feat: billing page in dashboard and setup.

This commit is contained in:
a.bouhuolia
2021-01-31 13:16:01 +02:00
parent 732c3bbfd7
commit e093be0663
60 changed files with 1505 additions and 1073 deletions

View File

@@ -1,41 +1,9 @@
// Subscription plans.
export const plans = [
{
name: 'basic',
description: [
'Sales/purchases module.',
'Expense module.',
'Inventory module.',
'Unlimited status pages.',
'Unlimited status pages.',
],
price: '1200',
slug: 'free',
currency: 'LYD',
},
{
name: 'pro',
description: [
'Sales/purchases module.',
'Expense module.',
'Inventory module.',
'Unlimited status pages.',
'Unlimited status pages.',
],
price: '1200',
slug: 'free',
currency: 'LYD',
},
];
export const paymentmethod = [
{
period: 'monthly',
price: '1200',
currency: 'LYD',
},
{
period: 'yearly',
price: '1200',
currency: 'LYD',
},
// Payment methods.
export const paymentMethods = [
];

View File

@@ -9,7 +9,7 @@ function DashboardSplitPane({
sidebarExpended,
children
}) {
const initialSize = 200;
const initialSize = 210;
const [defaultSize, setDefaultSize] = useState(
parseInt(localStorage.getItem('dashboard-size'), 10) || initialSize,

View File

@@ -12,6 +12,8 @@ import ReceiptNumberDialog from 'containers/Dialogs/ReceiptNumberDialog';
import InvoiceNumberDialog from 'containers/Dialogs/InvoiceNumberDialog';
import InventoryAdjustmentDialog from 'containers/Dialogs/InventoryAdjustmentFormDialog';
import PaymentViaVoucherDialog from 'containers/Dialogs/PaymentViaVoucherDialog';
export default function DialogsContainer() {
return (
<div>
@@ -26,6 +28,7 @@ export default function DialogsContainer() {
<ExchangeRateFormDialog dialogName={'exchangeRate-form'} />
<ItemCategoryDialog dialogName={'item-category-form'} />
<InventoryAdjustmentDialog dialogName={'inventory-adjustment-form'} />
<PaymentViaVoucherDialog dialogName={'payment-via-voucher'} />
</div>
);
}

View File

@@ -13,9 +13,9 @@ function EnsureOrganizationIsReady({
redirectTo = '/setup',
// #withOrganizationByOrgId
isOrganizationInitialized,
isOrganizationReady,
}) {
return (isOrganizationInitialized) ? children : (
return (isOrganizationReady) ? children : (
<Redirect
to={{ pathname: redirectTo }}
/>
@@ -27,5 +27,5 @@ export default compose(
connect((state, props) => ({
organizationId: props.currentOrganizationId,
})),
withOrganization(({ isOrganizationInitialized }) => ({ isOrganizationInitialized })),
withOrganization(({ isOrganizationReady }) => ({ isOrganizationReady })),
)(EnsureOrganizationIsReady);

View File

@@ -161,11 +161,11 @@ export default [
href: '/financial-reports/profit-loss-sheet',
},
{
text: 'Receivable Aging Summary',
text: 'A/R Aging Summary',
href: '/financial-reports/receivable-aging-summary',
},
{
text: 'Payable Aging Summary',
text: 'A/P Aging Summary',
href: '/financial-reports/payable-aging-summary',
},
],

View File

@@ -101,7 +101,7 @@ function RegisterUserForm({ requestRegister, requestLogin }) {
}),
});
}
if (errors.some((e) => e.type === 'EMAIL_EXISTS')) {
if (errors.some((e) => e.type === 'EMAIL.EXISTS')) {
setErrors({
email: formatMessage({
id: 'the_email_already_used_in_another_account',

View File

@@ -1,7 +1,7 @@
import React, { useCallback } from 'react';
import { Intent } from '@blueprintjs/core';
import { Formik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import { omit } from 'lodash';
import { useQuery, queryCache } from 'react-query';
import { AppToaster, DialogContent } from 'components';

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
};

View File

@@ -18,7 +18,7 @@ function SetupCongratsPage({
const handleBtnClick = useCallback(() => {
setOrganizationSetupCompleted(false);
history.push('/');
history.push('/homepage');
}, [
setOrganizationSetupCompleted,
history,

View 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>
)
}

View File

@@ -32,6 +32,7 @@ function SetupInitializingForm({
return (
<div class="setup-initializing-form">
<ProgressBar intent={Intent.PRIMARY} value={null} />
<div className={'setup-initializing-form__title'}>
<h1>
{/* You organization is initializin... */}

View File

@@ -1,9 +1,12 @@
import React, { useCallback } from 'react';
import { Icon, For } from 'components';
import { FormattedMessage as T } from 'react-intl';
import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions';
import withAuthentication from 'containers/Authentication/withAuthentication';
import footerLinks from 'config/footerLinks';
import { compose } from 'utils';

View File

@@ -1,7 +1,5 @@
import React, { useMemo, useState, useCallback } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { Row, Col } from 'react-grid-system';
import React from 'react';
import { FastField, Form, ErrorMessage } from 'formik';
import {
Button,
Intent,
@@ -11,429 +9,218 @@ import {
Classes,
Position,
} from '@blueprintjs/core';
import moment from 'moment';
import { DateInput } from '@blueprintjs/datetime';
import classNames from 'classnames';
import { TimezonePicker } from '@blueprintjs/timezone';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { DateInput } from '@blueprintjs/datetime';
import { withWizard } from 'react-albus';
import { FormattedMessage as T } from 'react-intl';
import 'style/pages/Setup/Organization.scss';
import { Col, Row, ListSelect } from 'components';
import {
momentFormatter,
tansformDateValue,
inputIntent,
handleDateChange
} from 'utils';
import { momentFormatter, tansformDateValue } from 'utils';
import { ListSelect, ErrorMessage, FieldRequiredHint } from 'components';
import fiscalYearOptions from 'common/fiscalYearOptions';
import languages from 'common/languagesOptions';
import currencies from 'common/currencies';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import { compose, optionsMapToArray } from 'utils';
function SetupOrganizationForm({
requestSubmitOptions,
requestOrganizationSeed,
wizard,
setOrganizationSetupCompleted
}) {
const { formatMessage } = useIntl();
const [selected, setSelected] = useState();
const baseCurrency = [
{ id: 0, name: 'LYD - Libyan Dinar', value: 'LYD' },
{ id: 1, name: 'USD - American Dollar', value: 'USD' },
];
const languages = [
{ id: 0, name: 'English', value: 'en' },
{ id: 1, name: 'Arabic', value: 'ar' },
];
const fiscalYear = [
{
id: 0,
name: `${formatMessage({ id: 'january' })} - ${formatMessage({
id: 'december',
})}`,
value: 'january',
},
{
id: 1,
name: `${formatMessage({ id: 'february' })} - ${formatMessage({
id: 'january',
})}`,
value: 'february',
},
{
id: 2,
name: `${formatMessage({ id: 'march' })} - ${formatMessage({
id: 'february',
})}`,
value: 'March',
},
{
id: 3,
name: `${formatMessage({ id: 'april' })} - ${formatMessage({
id: 'march',
})}`,
value: 'april',
},
{
id: 4,
name: `${formatMessage({ id: 'may' })} - ${formatMessage({
id: 'april',
})}`,
value: 'may',
},
{
id: 5,
name: `${formatMessage({ id: 'june' })} - ${formatMessage({
id: 'may',
})}`,
value: 'june',
},
{
id: 6,
name: `${formatMessage({ id: 'july' })} - ${formatMessage({
id: 'june',
})}`,
value: 'july',
},
{
id: 7,
name: `${formatMessage({ id: 'august' })} - ${formatMessage({
id: 'july',
})}`,
value: 'August',
},
{
id: 8,
name: `${formatMessage({ id: 'september' })} - ${formatMessage({
id: 'august',
})}`,
value: 'september',
},
{
id: 9,
name: `${formatMessage({ id: 'october' })} - ${formatMessage({
id: 'november',
})}`,
value: 'october',
},
{
id: 10,
name: `${formatMessage({ id: 'november' })} - ${formatMessage({
id: 'october',
})}`,
value: 'november',
},
{
id: 11,
name: `${formatMessage({ id: 'december' })} - ${formatMessage({
id: 'november',
})}`,
value: 'december',
},
];
const ValidationSchema = Yup.object().shape({
name: Yup.string()
.required()
.label(formatMessage({ id: 'organization_name_' })),
financial_date_start: Yup.date()
.required()
.label(formatMessage({ id: 'date_start_' })),
base_currency: Yup.string()
.required()
.label(formatMessage({ id: 'base_currency_' })),
language: Yup.string()
.required()
.label(formatMessage({ id: 'language' })),
fiscal_year: Yup.string()
.required()
.label(formatMessage({ id: 'fiscal_year_' })),
time_zone: Yup.string()
.required()
.label(formatMessage({ id: 'time_zone_' })),
});
const initialValues = useMemo(
() => ({
name: '',
financial_date_start: moment(new Date()).format('YYYY-MM-DD'),
base_currency: '',
language: '',
fiscal_year: '',
time_zone: '',
}),
[],
);
const {
values,
errors,
touched,
handleSubmit,
setFieldValue,
getFieldProps,
isSubmitting,
} = useFormik({
enableReinitialize: true,
validationSchema: ValidationSchema,
initialValues: {
...initialValues,
},
onSubmit: (values, { setSubmitting, setErrors }) => {
const options = optionsMapToArray(values).map((option) => {
return { key: option.key, ...option, group: 'organization' };
});
requestSubmitOptions({ options })
.then(() => {
return requestOrganizationSeed();
})
.then(() => {
return setOrganizationSetupCompleted(true);
})
.then((response) => {
setSubmitting(false);
wizard.next();
})
.catch((erros) => {
setSubmitting(false);
});
},
});
const onItemsSelect = (filedName) => {
return (filed) => {
setSelected({
...selected,
[filedName]: filed,
});
setFieldValue(filedName, filed.value);
};
};
const filterItems = (query, item, _index, exactMatch) => {
const normalizedTitle = item.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
const onItemRenderer = (item, { handleClick }) => (
<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 (
<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>
<Form>
<h3>
<T id={'organization_details'} />
</h3>
<form class="setup-organization__form" onSubmit={handleSubmit}>
<h3>
<T id={'organization_details'} />
</h3>
<FormGroup
label={<T id={'legal_organization_name'} />}
labelInfo={<FieldRequiredHint />}
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"
{/* ---------- Organization name ---------- */}
<FastField name={'name'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'legal_organization_name'} />}
className={'form-group--name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'organization_name'} />}
>
<T id={'save_continue'} />
</Button>
</div>
</form>
</div>
<InputGroup {...field} />
</FormGroup>
)}
</FastField>
{/* ---------- 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);

View 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);

View File

@@ -1,56 +1,52 @@
import React, { useCallback } from 'react';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import { Wizard, Steps, Step } from 'react-albus';
import { useHistory } from "react-router-dom";
import { connect } from 'react-redux';
import React from 'react';
import { Wizard } from 'react-albus';
import { useHistory } from 'react-router-dom';
import WizardSetupSteps from './WizardSetupSteps';
import withSubscriptions from 'containers/Subscriptions/withSubscriptions';
import SetupSubscriptionForm from './SetupSubscriptionForm';
import SetupOrganizationForm from './SetupOrganizationForm';
import SetupInitializingForm from './SetupInitializingForm';
import SetupCongratsPage from './SetupCongratsPage';
import SetupDialogs from './SetupDialogs';
import SetupWizardContent from './SetupWizardContent';
import withOrganization from 'containers/Organization/withOrganization';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import withSetupWizard from '../../store/organizations/withSetupWizard';
import withAuthentication from 'containers/Authentication/withAuthentication';
import withOrganization from 'containers/Organization/withOrganization'
import { compose } from 'utils';
/**
* Wizard setup right section.
*/
function SetupRightSection ({
// #withAuthentication
currentOrganizationId,
function SetupRightSection({
// #withOrganization
isOrganizationInitialized,
isOrganizationSeeded,
isOrganizationSetupCompleted,
// #withSetupWizard
isCongratsStep,
isSubscriptionStep,
isInitializingStep,
isOrganizationStep,
// #withSubscriptions
isSubscriptionActive
isSubscriptionActive,
}) {
const history = useHistory();
const handleSkip = useCallback(({ step, push }) => {
const handleSkip = ({ step, push }) => {
const scenarios = [
{ condition: isOrganizationSetupCompleted, redirectTo: 'congrats' },
{ condition: !isSubscriptionActive, redirectTo: 'subscription' },
{ condition: isSubscriptionActive && !isOrganizationInitialized, redirectTo: 'initializing' },
{ condition: isSubscriptionActive && !isOrganizationSeeded, redirectTo: 'organization' },
{ condition: isCongratsStep, redirectTo: 'congrats' },
{ condition: isSubscriptionStep, redirectTo: 'subscription' },
{ condition: isInitializingStep, redirectTo: 'initializing' },
{ condition: isOrganizationStep, redirectTo: 'organization' },
];
const scenario = scenarios.find((scenario) => scenario.condition);
if (scenario) {
push(scenario.redirectTo);
}
}, [
isSubscriptionActive,
isOrganizationInitialized,
isOrganizationSeeded,
isOrganizationSetupCompleted
]);
};
return (
<section className={'setup-page__right-section'}>
@@ -58,58 +54,47 @@ function SetupRightSection ({
onNext={handleSkip}
basename={'/setup'}
history={history}
render={({ step, steps }) => (
<div class="setup-page__content">
<WizardSetupSteps currentStep={steps.indexOf(step) + 1} />
<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>
)} />
render={SetupWizardContent}
/>
<SetupDialogs />
</section>
)
);
}
export default compose(
withAuthentication(({ currentOrganizationId }) => ({ currentOrganizationId })),
connect((state, props) => ({
organizationId: props.currentOrganizationId,
withCurrentOrganization(({ organizationTenantId }) => ({
organizationId: organizationTenantId,
})),
withOrganization(({
organization,
isOrganizationInitialized,
isOrganizationSeeded,
isOrganizationSetupCompleted
}) => ({
organization,
isOrganizationInitialized,
isOrganizationSeeded,
isOrganizationSetupCompleted
})),
withSubscriptions(({
isSubscriptionActive,
}) => ({
isSubscriptionActive
}), 'main'),
)(SetupRightSection);
withOrganization(
({
organization,
isOrganizationInitialized,
isOrganizationSeeded,
isOrganizationSetupCompleted,
}) => ({
organization,
isOrganizationInitialized,
isOrganizationSeeded,
isOrganizationSetupCompleted,
}),
),
withSubscriptions(
({ isSubscriptionActive }) => ({
isSubscriptionActive,
}),
'main',
),
withSetupWizard(
({
isCongratsStep,
isSubscriptionStep,
isInitializingStep,
isOrganizationStep,
}) => ({
isCongratsStep,
isSubscriptionStep,
isInitializingStep,
isOrganizationStep,
}),
),
)(SetupRightSection);

View 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);

View File

@@ -1,98 +1,15 @@
import React, { useMemo } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { Button, Intent } from '@blueprintjs/core';
import { withWizard } from 'react-albus';
import React from 'react';
import { Form } from 'formik';
import 'style/pages/Setup/Billing.scss';
import BillingPlans from 'containers/Subscriptions/billingPlans';
import BillingPeriods from 'containers/Subscriptions/billingPeriods';
import { BillingPaymentmethod } from 'containers/Subscriptions/billingPaymentmethod';
import withSubscriptionsActions from 'containers/Subscriptions/withSubscriptionsActions';
import withBillingActions from 'containers/Subscriptions/withBillingActions';
import { compose } from 'utils';
import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm';
/**
* Subscription step of wizard setup.
*/
function SetupSubscriptionForm({
// #withBillingActions
requestSubmitBilling,
// #withWizard
wizard,
// #withSubscriptionsActions
requestFetchSubscriptions
}) {
const { formatMessage } = useIntl();
const validationSchema = Yup.object().shape({
plan_slug: Yup.string()
.required()
.label(formatMessage({ id: 'plan_slug' })),
license_code: Yup.string()
.min(10)
.max(10)
.required()
.label(formatMessage({ id: 'license_code_' }))
.trim(),
});
const initialValues = useMemo(
() => ({
plan_slug: '',
license_code: '',
}),
[],
);
const formik = useFormik({
enableReinitialize: true,
validationSchema: validationSchema,
initialValues: {
...initialValues,
},
onSubmit: (values, { setSubmitting, setErrors }) => {
requestSubmitBilling(values)
.then((response) => {
return requestFetchSubscriptions();
})
.then(() => {
wizard.next();
setSubmitting(false);
})
.catch((errors) => {
setSubmitting(false);
});
},
});
export default function SetupSubscriptionForm() {
return (
<div className={'register-subscription-form'}>
<form onSubmit={formik.handleSubmit} className={'billing-form'}>
<BillingPlans title={'a_select_a_plan'} formik={formik} />
<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>
<Form>
<BillingPlansForm />
</Form>
);
}
export default compose(
withBillingActions,
withWizard,
withSubscriptionsActions,
)(SetupSubscriptionForm);

View 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>
);
}

View 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(),
});

View File

@@ -1,13 +1,21 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect } from 'react';
import * as Yup from 'yup';
import { useFormik } from 'formik';
import { Button, Intent } from '@blueprintjs/core';
import { Formik, Form } from 'formik';
import { FormattedMessage as T, useIntl } from 'react-intl';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { MeteredBillingTabs, PaymentMethodTabs } from './SubscriptionTabs';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import 'style/pages/Billing/BillingPage.scss';
import { MasterBillingTabs } from './SubscriptionTabs';
import withBillingActions from './withBillingActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import { compose } from 'utils';
/**
* Billing form.
*/
function BillingForm({
// #withDashboardActions
changePageTitle,
@@ -23,51 +31,43 @@ function BillingForm({
const validationSchema = Yup.object().shape({
plan_slug: Yup.string()
.required()
.label(formatMessage({ id: 'plan_slug' })),
.required(),
period: Yup.string().required(),
license_code: Yup.string().trim(),
});
const initialValues = useMemo(
() => ({
plan_slug: 'free',
license_code: '',
}),
[],
);
const initialValues = {
plan_slug: 'free',
period: 'month',
license_code: '',
};
const formik = useFormik({
enableReinitialize: true,
validationSchema: validationSchema,
initialValues: {
...initialValues,
},
onSubmit: (values, { setSubmitting, resetForm, setErrors }) => {
requestSubmitBilling(values)
.then((response) => {
setSubmitting(false);
})
.catch((errors) => {
setSubmitting(false);
});
},
});
const handleSubmit = (values, { setSubmitting }) => {
requestSubmitBilling(values)
.then((response) => {
setSubmitting(false);
})
.catch((errors) => {
setSubmitting(false);
});
};
return (
<div className={'billing-form'}>
<form onSubmit={formik.handleSubmit}>
<MeteredBillingTabs formik={formik} planId={formik.values.plan_slug} />
<div className={'subscribe-button'}>
<Button
intent={Intent.PRIMARY}
type="submit"
loading={formik.isSubmitting}
>
<T id={'subscribe'} />
</Button>
</div>
</form>
</div>
<DashboardInsider name={'billing-page'}>
<div className={'billing-page'}>
<Formik
validationSchema={validationSchema}
onSubmit={handleSubmit}
initialValues={initialValues}
>
{({ isSubmitting, handleSubmit }) => (
<Form>
<MasterBillingTabs />
</Form>
)}
</Formik>
</div>
</DashboardInsider>
);
}

View 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);

View 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,
);

View 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>
);
}

View 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>
)
}

View 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);

View File

@@ -1,16 +1,10 @@
import React from 'react';
import BillingPlans from 'containers/Subscriptions/billingPlans';
import BillingPeriods from 'containers/Subscriptions/billingPeriods';
import { BillingPaymentmethod } from 'containers/Subscriptions/billingPaymentmethod';
import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm';
function BillingTab({ formik }) {
export default function BillingTab() {
return (
<div>
<BillingPlans title={'a_select_a_plan'} formik={formik} />
<BillingPeriods title={'b_choose_your_billing'} formik={formik} />
<BillingPaymentmethod title={'c_payment_methods'} formik={formik} />
<BillingPlansForm />
</div>
);
}
export default BillingTab;
}

View File

@@ -1,35 +1,38 @@
import React from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { InputGroup, FormGroup, Intent } from '@blueprintjs/core';
import ErrorMessage from 'components/ErrorMessage';
import { FormattedMessage as T } from 'react-intl';
import { Intent, Button } from '@blueprintjs/core';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'redux';
import { useFormikContext } from 'formik';
/**
* Payment via license code tab.
*/
function LicenseTab({ openDialog }) {
const { submitForm, values } = useFormikContext();
const handleSubmitBtnClick = () => {
submitForm().then(() => {
openDialog('payment-via-voucher', { ...values });
});
};
function LicenseTab({
formik: { errors, touched, setFieldValue, getFieldProps },
}) {
return (
<div className={'license-container'}>
<h4>
<T id={'license_code'} />
</h4>
<h3>
<T id={'voucher'} />
</h3>
<p className="paragraph">
<T id={'cards_will_be_charged'} />
</p>
<FormGroup
label={<T id={'license_number'} />}
intent={errors.license_code && touched.license_code && Intent.DANGER}
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>
<Button onClick={handleSubmitBtnClick} intent={Intent.PRIMARY} large={true}>
Submit Voucher
</Button>
</div>
);
}
export default LicenseTab;
export default compose(withDialogActions)(LicenseTab);

View File

@@ -1,16 +1,18 @@
import React, { useState } from 'react';
import React from 'react';
import { Tabs, Tab } from '@blueprintjs/core';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import BillingTab from './BillingTab';
import LicenseTab from './LicenseTab';
export const MeteredBillingTabs = ({ formik }) => {
const [animate, setAnimate] = useState(true);
/**
* Master billing tabs.
*/
export const MasterBillingTabs = ({ formik }) => {
const { formatMessage } = useIntl();
return (
<div>
<Tabs animate={animate} large={true}>
<Tabs animate={true} large={true}>
<Tab
title={formatMessage({ id: 'billing' })}
id={'billing'}
@@ -20,23 +22,24 @@ export const MeteredBillingTabs = ({ formik }) => {
title={formatMessage({ id: 'usage' })}
id={'usage'}
disabled={true}
// panel={'Usage'}
/>
</Tabs>
</div>
);
};
/**
* Payment methods tabs.
*/
export const PaymentMethodTabs = ({ formik }) => {
const [animate, setAnimate] = useState(true);
const { formatMessage } = useIntl();
return (
<div>
<Tabs animate={animate} large={true}>
<Tabs animate={true} large={true}>
<Tab
title={formatMessage({ id: 'license' })}
id={'license'}
title={formatMessage({ id: 'voucher' })}
id={'voucher'}
panel={<LicenseTab formik={formik} />}
/>
<Tab

View File

@@ -1,16 +1,12 @@
import React, { useState, useRef, useEffect } from 'react';
import { FormattedMessage as T, useIntl } from 'react-intl';
import React from 'react';
import { PaymentMethodTabs } from './SubscriptionTabs';
export const BillingPaymentmethod = ({ formik, title }) => {
export default ({ formik, title, description }) => {
return (
<section class="billing-section">
<h1 className={'bg-title'}>
<T id={title} />
</h1>
<p className='paragraph'>
<T id={'please_enter_your_preferred_payment_method'} />
</p>
<section class="billing-plans__section">
<h1 className="title">{ title }</h1>
<p className="paragraph">{ description }</p>
<PaymentMethodTabs formik={formik} />
</section>
);

View File

@@ -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;

View File

@@ -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;

View 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);
};

View 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);
};

View File

@@ -353,8 +353,8 @@ export default {
once_delete_these_exchange_rates_you_will_not_able_restore_them: `Once you delete these exchange rates, you won't be able to retrieve them later. Are you sure you want to delete them?`,
once_delete_this_item_category_you_will_able_to_restore_it: `Once you delete this category, you won\'t be able to restore it later. Are you sure you want to delete this item?`,
select_business_location: 'Select Business Location',
select_base_currency: 'Select Base Currency',
select_fiscal_year: 'Select Fiscal Year',
select_base_currency: 'Select base currency',
select_fiscal_year: 'Select fiscal year',
select_language: 'Select Language',
select_date_format: 'Select Date Format',
select_time_zone: 'Select Time Zone',
@@ -700,9 +700,9 @@ export default {
billing: 'Billing',
the_billing_has_been_created_successfully:
'The billing has been created successfully.',
a_select_a_plan: 'A. Select a plan',
b_choose_your_billing: 'B. Choose your billing',
c_payment_methods: 'C. Payment methods',
select_a_plan: '{order}. Select a plan',
choose_your_billing: '{order}. Choose your billing',
payment_methods: '{order}. Payment methods',
usage: 'Usage',
basic: 'Basic',
license: 'License',
@@ -964,4 +964,7 @@ export default {
selected_customers: '{count} Selected Customers',
transaction_number: 'Transaction #',
running_balance: 'Running balance',
payment_via_voucher: 'Payment via voucher',
voucher_number: 'Voucher number',
voucher: 'Voucher'
};

View File

@@ -404,5 +404,5 @@ export default [
loader: () => import('containers/Purchases/PaymentMades/PaymentMadeList'),
}),
breadcrumb: 'Payment Made List',
},
}
];

View File

@@ -40,11 +40,11 @@ export const seedTenant = () => (dispatch, getState) => new Promise((resolve, re
payload: { organizationId }
});
ApiService.post(`organization/seed/`).then((response) => {
resolve(response);
dispatch({
type: t.SET_ORGANIZATION_SEEDED,
payload: { organizationId }
});
resolve(response);
})
.catch((error) => {
reject(error.response.data.errors || []);

View File

@@ -54,4 +54,4 @@ export const isOrganizationCongratsFactory = () => createSelector(
(organization) => {
return !!organization?.is_congrats;
}
)
);

View 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);
};

View 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, {
});

View 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,
)

View File

@@ -28,6 +28,7 @@ import paymentMades from './PaymentMades/paymentMade.reducer';
import organizations from './organizations/organizations.reducers';
import subscriptions from './subscription/subscription.reducer';
import inventoryAdjustments from './inventoryAdjustments/inventoryAdjustment.reducer';
import plans from './plans/plans.reducer';
export default combineReducers({
authentication,
@@ -58,4 +59,5 @@ export default combineReducers({
paymentReceives,
paymentMades,
inventoryAdjustments,
plans
});

View File

@@ -95,4 +95,14 @@ body.hide-scrollbar .Pane2{
button{
justify-content: start;
}
}
.bp3-timezone-picker{
.bp3-button{
[icon="caret-down"] {
display: none;
}
}
}

View File

@@ -57,7 +57,7 @@
.#{$ns}-menu-item {
color: $sidebar-menu-item-color;
border-radius: 0;
padding: 8px 18px;
padding: 8px 20px;
font-size: 15px;
font-weight: 400;
@@ -84,7 +84,7 @@
display: block;
color: $sidebar-menu-label-color;
font-size: 11px;
padding: 10px 18px;
padding: 10px 20px;
margin-top: 4px;
text-transform: uppercase;
font-weight: 500;
@@ -104,7 +104,7 @@
padding-top: 6px;
}
.#{$ns}-menu-item {
padding: 8px 16px;
padding: 8px 20px;
font-size: 15px;
color: $sidebar-submenu-item-color;

View 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
}
}
}

View File

@@ -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%;
}
}
}

View File

@@ -5,7 +5,7 @@
padding: 45px 0 20px;
&__title-wrap {
margin-bottom: 32px;
margin-bottom: 20px;
h1 {
margin-top: 0;
@@ -17,17 +17,24 @@
}
}
&__form {
form {
h3 {
margin-bottom: 1rem;
}
}
.bp3-form-group {
margin-bottom: 24px;
.bp3-input-group {
.bp3-input {
height: 38px;
}
}
.bp3-input,
.form-group--select-list .bp3-button{
font-size: 15px;
}
}

View 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;
}
}
}
}

View File

@@ -59,7 +59,7 @@
&__title {
font-size: 26px;
font-weight: 700;
font-weight: 600;
line-height: normal;
margin-bottom: 20px;
margin-top: 14px;
@@ -131,11 +131,9 @@
margin: 0 auto;
padding: 50px 0 0;
}
ul {
display: flex;
}
li {
position: relative;
list-style-type: none;
@@ -155,7 +153,6 @@
border-radius: 50%;
background-color: #75859c;
}
&::after {
width: 100%;
height: 2px;
@@ -166,23 +163,19 @@
left: -50%;
z-index: -1;
}
&:first-child::after {
display: none;
}
&.is-active {
&::before {
background-color: #75859c;
}
~ li {
&:before,
&:after {
background: #ebebeb;
}
}
p.wizard-info {
color: #004dd0;
}

View File

@@ -0,0 +1,7 @@
.setup-subscription-form{
max-width: 800px;
margin: 0 auto;
padding: 0 60px;
margin-top: 40px;
}

View 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;
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View File

@@ -28,7 +28,6 @@ $button-background-color-disabled: #E9ECEF !default;
$button-background-color: #E6EFFB !default;
$button-background-color-hover: #CFDCEE !default;
$sidebar-background: #01115e;
$sidebar-text-color: #fff;
$sidebar-width: 100%;

View File

@@ -407,6 +407,9 @@ export const transformToCamelCase = (object) => {
return deepMapKeys(object, (key) => _.snakeCase(key));
};
export const transfromToSnakeCase = (object) => {
return deepMapKeys(object, (key) => _.snakeCase(key));
};
export function flatObject(obj) {
const flatObject = {};
@@ -427,4 +430,5 @@ export function flatObject(obj) {
dig(obj);
return flatObject;
}
}

View File

@@ -79,7 +79,7 @@ export default class SettingsController extends BaseController{
* @param {Request} req -
* @param {Response} res -
*/
saveSettings(req: Request, res: Response) {
async saveSettings(req: Request, res: Response) {
const { Option } = req.models;
const optionsDTO: IOptionsDTO = this.matchedBodyData(req);
const { settings } = req;
@@ -102,6 +102,8 @@ export default class SettingsController extends BaseController{
});
this.observeAppConfigsComplete(settings);
await settings.save();
return res.status(200).send({
type: 'success',
code: 'OPTIONS.SAVED.SUCCESSFULLY',

View File

@@ -4,7 +4,6 @@ import SettingsStore from 'services/Settings/SettingsStore';
export default async (req: Request, res: Response, next: NextFunction) => {
const { tenantId } = req.user;
const { knex } = req;
const Logger = Container.get('logger');
const tenantContainer = Container.of(`tenant-${tenantId}`);