feat: ensure to access dashboard user's subscription is active.

This commit is contained in:
a.bouhuolia
2021-08-30 10:42:39 +02:00
parent 47da64e625
commit 9dca9f3317
59 changed files with 1299 additions and 546 deletions

View File

@@ -2,15 +2,15 @@ import intl from 'react-intl-universal';
export const getSetupWizardSteps = () => [
{
label: intl.get('Plans & Payment'),
label: intl.get('setup.plan.plans'),
},
{
label: intl.get('Initializing'),
label: intl.get('setup.plan.initializing'),
},
{
label: intl.get('Getting started'),
label: intl.get('setup.plan.getting_started'),
},
{
label: intl.get('Congratulations'),
label: intl.get('setup.plan.congrats'),
},
];

View File

@@ -0,0 +1,17 @@
import React from 'react';
import clsx from 'classnames';
import Style from './style.module.scss';
export function Alert({ title, description, intent }) {
return (
<div
className={clsx(Style.root, {
[`${Style['root_' + intent]}`]: intent,
})}
>
{title && <h3 className={clsx(Style.title)}>{title}</h3>}
{description && <p class={clsx(Style.description)}>{description}</p>}
</div>
);
}

View File

@@ -0,0 +1,32 @@
.root {
border: 1px solid rgb(223, 227, 230);
padding: 12px;
border-radius: 6px;
margin-bottom: 20px;
&_danger {
border-color: rgb(249, 198, 198);
background: rgb(255, 248, 248);
.description {
color: #d95759;
}
.title {
color: rgb(205, 43, 49);
}
}
}
.title {
color: rgb(17, 24, 28);
margin-bottom: 4px;
font-size: 14px;
font-weight: 600;
}
.description {
color: rgb(104, 112, 118);
margin: 0;
}

View File

@@ -12,6 +12,33 @@ import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane';
import GlobalHotkeys from './GlobalHotkeys';
import DashboardProvider from './DashboardProvider';
import DrawersContainer from 'components/DrawersContainer';
import EnsureSubscriptionIsActive from '../Guards/EnsureSubscriptionIsActive';
/**
* Dashboard preferences.
*/
function DashboardPreferences() {
return (
<EnsureSubscriptionIsActive>
<DashboardSplitPane>
<Sidebar />
<PreferencesPage />
</DashboardSplitPane>
</EnsureSubscriptionIsActive>
);
}
/**
* Dashboard other routes.
*/
function DashboardAnyPage() {
return (
<DashboardSplitPane>
<Sidebar />
<DashboardContent />
</DashboardSplitPane>
);
}
/**
* Dashboard page.
@@ -20,19 +47,8 @@ export default function Dashboard() {
return (
<DashboardProvider>
<Switch>
<Route path="/preferences">
<DashboardSplitPane>
<Sidebar />
<PreferencesPage />
</DashboardSplitPane>
</Route>
<Route path="/">
<DashboardSplitPane>
<Sidebar />
<DashboardContent />
</DashboardSplitPane>
</Route>
<Route path="/preferences" component={DashboardPreferences} />
<Route path="/" component={DashboardAnyPage} />
</Switch>
<DashboardUniversalSearch />

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import DashboardTopbar from 'components/Dashboard/DashboardTopbar';
import DashboardContentRoute from 'components/Dashboard/DashboardContentRoute';
import DashboardContentRoutes from 'components/Dashboard/DashboardContentRoute';
import DashboardFooter from 'components/Dashboard/DashboardFooter';
import DashboardErrorBoundary from './DashboardErrorBoundary';
@@ -10,7 +10,7 @@ export default React.forwardRef(({}, ref) => {
<ErrorBoundary FallbackComponent={DashboardErrorBoundary}>
<div className="dashboard-content" id="dashboard" ref={ref}>
<DashboardTopbar />
<DashboardContentRoute />
<DashboardContentRoutes />
<DashboardFooter />
</div>
</ErrorBoundary>

View File

@@ -2,8 +2,43 @@ import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { getDashboardRoutes } from 'routes/dashboard';
import EnsureSubscriptionsIsActive from '../Guards/EnsureSubscriptionsIsActive';
import EnsureSubscriptionsIsInactive from '../Guards/EnsureSubscriptionsIsInactive';
import DashboardPage from './DashboardPage';
/**
* Dashboard inner route content.
*/
function DashboardContentRouteContent({ route }) {
const content = (
<DashboardPage
name={route.name}
Component={route.component}
pageTitle={route.pageTitle}
backLink={route.backLink}
hint={route.hint}
sidebarExpand={route.sidebarExpand}
pageType={route.pageType}
defaultSearchResource={route.defaultSearchResource}
/>
);
return route.subscriptionActive ? (
<EnsureSubscriptionsIsInactive
subscriptionTypes={route.subscriptionActive}
children={content}
redirectTo={'/billing'}
/>
) : route.subscriptionInactive ? (
<EnsureSubscriptionsIsActive
subscriptionTypes={route.subscriptionInactive}
children={content}
redirectTo={'/'}
/>
) : (
content
);
}
/**
* Dashboard content route.
*/
@@ -14,21 +49,8 @@ export default function DashboardContentRoute() {
<Route pathname="/">
<Switch>
{routes.map((route, index) => (
<Route
exact={route.exact}
key={index}
path={`${route.path}`}
>
<DashboardPage
name={route.name}
Component={route.component}
pageTitle={route.pageTitle}
backLink={route.backLink}
hint={route.hint}
sidebarExpand={route.sidebarExpand}
pageType={route.pageType}
defaultSearchResource={route.defaultSearchResource}
/>
<Route exact={route.exact} key={index} path={`${route.path}`}>
<DashboardContentRouteContent route={route} />
</Route>
))}
</Switch>

View File

@@ -1,15 +1,12 @@
import React from 'react';
import DashboardLoadingIndicator from './DashboardLoadingIndicator';
import { useSettings } from 'hooks/query';
/**
* Dashboard provider.
*/
export default function DashboardProvider({ children }) {
const { isLoading } = useSettings();
return (
<DashboardLoadingIndicator isLoading={isLoading}>
<DashboardLoadingIndicator isLoading={false}>
{ children }
</DashboardLoadingIndicator>
)

View File

@@ -23,6 +23,43 @@ import withSettings from 'containers/Settings/withSettings';
import QuickNewDropdown from 'containers/QuickNewDropdown/QuickNewDropdown';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
function DashboardTopbarSubscriptionMessage() {
return (
<div class="dashboard__topbar-subscription-msg">
<span>
<T id={'dashboard.subscription_msg.period_over'} />
</span>
</div>
);
}
function DashboardHamburgerButton({ ...props }) {
return (
<Button minimal={true} {...props}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
role="img"
focusable="false"
>
<title>
<T id={'menu'} />
</title>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-miterlimit="5"
stroke-width="2"
d="M4 7h15M4 12h15M4 17h15"
></path>
</svg>
</Button>
);
}
/**
* Dashboard topbar.
@@ -44,6 +81,10 @@ function DashboardTopbar({
// #withGlobalSearch
openGlobalSearch,
// #withSubscriptions
isSubscriptionActive,
isSubscriptionInactive,
}) {
const history = useHistory();
@@ -69,27 +110,7 @@ function DashboardTopbar({
}
position={Position.RIGHT}
>
<Button minimal={true} onClick={handleSidebarToggleBtn}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
role="img"
focusable="false"
>
<title>
<T id={'menu'} />
</title>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-miterlimit="5"
stroke-width="2"
d="M4 7h15M4 12h15M4 17h15"
></path>
</svg>
</Button>
<DashboardHamburgerButton onClick={handleSidebarToggleBtn} />
</Tooltip>
</div>
@@ -114,13 +135,17 @@ function DashboardTopbar({
<div class="dashboard__breadcrumbs">
<DashboardBreadcrumbs />
</div>
<DashboardBackLink />
</div>
<div class="dashboard__topbar-right">
<If condition={isSubscriptionInactive}>
<DashboardTopbarSubscriptionMessage />
</If>
<Navbar class="dashboard__topbar-navbar">
<NavbarGroup>
<If condition={isSubscriptionActive}>
<Button
onClick={() => openGlobalSearch(true)}
className={Classes.MINIMAL}
@@ -128,6 +153,7 @@ function DashboardTopbar({
text={<T id={'quick_find'} />}
/>
<QuickNewDropdown />
<Tooltip
content={<T id={'notifications'} />}
position={Position.BOTTOM}
@@ -137,6 +163,8 @@ function DashboardTopbar({
icon={<Icon icon={'notification-24'} iconSize={20} />}
/>
</Tooltip>
</If>
<Button
className={Classes.MINIMAL}
icon={<Icon icon={'help-24'} iconSize={20} />}
@@ -166,4 +194,11 @@ export default compose(
organizationName: organizationSettings.name,
})),
withDashboardActions,
withSubscriptions(
({ isSubscriptionActive, isSubscriptionInactive }) => ({
isSubscriptionActive,
isSubscriptionInactive,
}),
'main',
),
)(DashboardTopbar);

View File

@@ -8,15 +8,21 @@ import {
Popover,
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'components';
import { If, FormattedMessage as T } from 'components';
import { firstLettersArgs } from 'utils';
import { useAuthActions, useAuthUser } from 'hooks/state';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
function DashboardTopbarUser({ openDialog }) {
function DashboardTopbarUser({
openDialog,
// #withSubscriptions
isSubscriptionActive
}) {
const history = useHistory();
const { setLogout } = useAuthActions();
const user = useAuthUser();
@@ -48,6 +54,7 @@ function DashboardTopbarUser({ openDialog }) {
}
/>
<MenuDivider />
<If condition={isSubscriptionActive}>
<MenuItem
text={<T id={'keyboard_shortcuts'} />}
onClick={onKeyboardShortcut}
@@ -56,6 +63,7 @@ function DashboardTopbarUser({ openDialog }) {
text={<T id={'preferences'} />}
onClick={() => history.push('/preferences')}
/>
</If>
<MenuItem text={<T id={'logout'} />} onClick={onClickLogout} />
</Menu>
}
@@ -69,4 +77,10 @@ function DashboardTopbarUser({ openDialog }) {
</Popover>
);
}
export default compose(withDialogActions)(DashboardTopbarUser);
export default compose(
withDialogActions,
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(DashboardTopbarUser);

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { includes } from 'lodash';
import { compose } from 'utils';
import { Redirect } from 'react-router-dom';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
/**
* Ensures the given subscription type is active or redirect to the given route.
*/
function EnsureSubscriptionIsActive({
children,
subscriptionType = 'main',
redirectTo = '/billing',
routePath,
exclude,
isSubscriptionActive,
}) {
return isSubscriptionActive || includes(exclude, routePath) ? (
children
) : (
<Redirect to={{ pathname: redirectTo }} />
);
}
export default compose(
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(EnsureSubscriptionIsActive);

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { includes } from 'lodash';
import { compose } from 'utils';
import { Redirect } from 'react-router-dom';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptionss';
/**
* Ensures the given subscription type is active or redirect to the given route.
*/
function EnsureSubscriptionsIsActive({
children,
subscriptionType = 'main',
redirectTo = '/billing',
routePath,
exclude,
isSubscriptionsActive,
}) {
return !isSubscriptionsActive || includes(exclude, routePath) ? (
children
) : (
<Redirect to={{ pathname: redirectTo }} />
);
}
export default compose(
withSubscriptions(
({ isSubscriptionsActive }) => ({ isSubscriptionsActive }),
'main',
),
)(EnsureSubscriptionsIsActive);

View File

@@ -0,0 +1,31 @@
import React from 'react';
import { includes } from 'lodash';
import { compose } from 'utils';
import { Redirect } from 'react-router-dom';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptionss';
/**
* Ensures the given subscription type is active or redirect to the given route.
*/
function EnsureSubscriptionsIsInactive({
children,
subscriptionType = 'main',
redirectTo = '/billing',
routePath,
exclude,
isSubscriptionsInactive,
}) {
return !isSubscriptionsInactive || includes(exclude, routePath) ? (
children
) : (
<Redirect to={{ pathname: redirectTo }} />
);
}
export default compose(
withSubscriptions(
({ isSubscriptionsInactive }) => ({ isSubscriptionsInactive }),
'main',
),
)(EnsureSubscriptionsIsInactive);

View File

@@ -1,10 +1,8 @@
import React from 'react';
import { Route, Switch, Redirect } from 'react-router-dom';
import preferencesRoutes from 'routes/preferences'
import { Route, Switch } from 'react-router-dom';
import preferencesRoutes from 'routes/preferences';
export default function DashboardContentRoute() {
return (
<Route pathname="/preferences">
<Switch>

View File

@@ -5,6 +5,7 @@ import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withDashboard from 'containers/Dashboard/withDashboard';
import { compose } from 'utils';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
function SidebarContainer({
// #ownProps
@@ -15,6 +16,9 @@ function SidebarContainer({
// #withDashboard
sidebarExpended,
// #withSubscription
isSubscriptionActive,
}) {
const sidebarScrollerRef = React.useRef();
@@ -31,7 +35,7 @@ function SidebarContainer({
const handleSidebarMouseLeave = () => {
if (!sidebarExpended && sidebarScrollerRef.current) {
sidebarScrollerRef.current.scrollTo({ top: 0, left: 0, });
sidebarScrollerRef.current.scrollTo({ top: 0, left: 0 });
}
};
@@ -43,6 +47,7 @@ function SidebarContainer({
<div
className={classNames('sidebar', {
'sidebar--mini-sidebar': !sidebarExpended,
'is-subscription-inactive': !isSubscriptionActive,
})}
id="sidebar"
onMouseLeave={handleSidebarMouseLeave}
@@ -64,4 +69,8 @@ export default compose(
withDashboard(({ sidebarExpended }) => ({
sidebarExpended,
})),
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(SidebarContainer);

View File

@@ -8,6 +8,8 @@ import MenuItem from 'components/MenuItem';
import { MenuItemLabel } from 'components';
import classNames from 'classnames';
import SidebarOverlay from 'components/SidebarOverlay';
import { compose } from 'redux';
import withSubscriptions from '../../containers/Subscriptions/withSubscriptions';
const DEFAULT_ITEM = {
text: '',
@@ -19,12 +21,10 @@ function matchPath(pathname, path, matchExact) {
}
function SidebarMenuItemSpace({ space }) {
return (
<div class="bp3-menu-spacer" style={{ height: `${space}px` }} />
)
return <div class="bp3-menu-spacer" style={{ height: `${space}px` }} />;
}
export default function SidebarMenu() {
function SidebarMenu({ isSubscriptionActive }) {
const history = useHistory();
const location = useLocation();
@@ -92,7 +92,11 @@ export default function SidebarMenu() {
);
});
};
const items = menuItemsMapper(sidebarMenuList);
const filterItems = sidebarMenuList.filter(
(item) => isSubscriptionActive || item.enableBilling,
);
const items = menuItemsMapper(filterItems);
const handleSidebarOverlayClose = () => {
setIsOpen(false);
@@ -110,3 +114,10 @@ export default function SidebarMenu() {
</div>
);
}
export default compose(
withSubscriptions(
({ isSubscriptionActive }) => ({ isSubscriptionActive }),
'main',
),
)(SidebarMenu);

View File

@@ -0,0 +1,135 @@
import React from 'react';
import classNames from 'classnames';
import { T } from 'components';
import { saveInvoke } from 'utils';
import 'style/pages/Subscription/PlanRadio.scss';
import 'style/pages/Subscription/PlanPeriodRadio.scss';
export function SubscriptionPlans({ value, plans, onSelect }) {
const handleSelect = (value) => {
onSelect && onSelect(value);
};
return (
<div className={'plan-radios'}>
{plans.map((plan) => (
<SubscriptionPlan
name={plan.name}
description={plan.description}
slug={plan.slug}
price={plan.price}
currencyCode={plan.currencyCode}
value={plan.slug}
onSelected={handleSelect}
selectedOption={value}
/>
))}
</div>
);
}
export function SubscriptionPlan({
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'}>{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>
);
}
/**
* Subscription periods.
*/
export function SubscriptionPeriods({ periods, selectedPeriod, onPeriodSelect }) {
const handleSelected = (value) => {
saveInvoke(onPeriodSelect, value);
};
return (
<div className={'plan-periods'}>
{periods.map((period) => (
<SubscriptionPeriod
period={period.slug}
label={period.label}
onSelected={handleSelected}
price={period.price}
selectedPeriod={selectedPeriod}
/>
))}
</div>
);
}
/**
* Billing period.
*/
export function SubscriptionPeriod({
// #ownProps
label,
selectedPeriod,
onSelected,
period,
price,
currencyCode,
}) {
const handlePeriodClick = () => {
saveInvoke(onSelected, period);
};
return (
<div
id={`plan-period-${period}`}
className={classNames(
{ 'is-selected': period === selectedPeriod },
'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>
);
}

View File

@@ -72,6 +72,8 @@ export * from './Details';
export * from './Drawer/DrawerInsider';
export * from './Drawer/DrawerMainTabs';
export * from './TotalLines/index'
export * from './Alert';
export * from './Subscriptions';
const Hint = FieldHint;

View File

@@ -298,6 +298,7 @@ export default [
},
{
text: <T id={'system'} />,
enableBilling: true,
label: true,
},
{
@@ -307,5 +308,6 @@ export default [
{
text: <T id={'billing'} />,
href: '/billing',
enableBilling: true,
},
];

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { Formik } from 'formik';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import * as Yup from 'yup';
import { useHistory } from 'react-router-dom';
import Toaster from 'components/AppToaster';
import 'style/pages/Setup/PaymentViaVoucherDialog.scss';
@@ -14,7 +15,6 @@ import PaymentViaLicenseForm from './PaymentViaVoucherForm';
import withDialogActions from 'containers/Dialog/withDialogActions';
import { compose } from 'utils';
import { Intent } from '@blueprintjs/core';
/**
* Payment via license dialog content.
@@ -35,10 +35,11 @@ function PaymentViaLicenseDialogContent({
const handleSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
// Payment via voucher mutate.
paymentViaVoucherMutate({ ...values })
.then(() => {
Toaster.show({
message: intl.get('payment_has_been_done_successfully'),
message: intl.get('payment_via_voucher.success_message'),
intent: Intent.SUCCESS,
});
return closeDialog('payment-via-voucher');
@@ -54,7 +55,7 @@ function PaymentViaLicenseDialogContent({
}) => {
if (errors.find((e) => e.type === 'LICENSE.CODE.IS.INVALID')) {
setErrors({
license_code: 'The license code is not valid, please try agin.',
license_code: 'payment_via_voucher.license_code_not_valid',
});
}
},

View File

@@ -31,7 +31,7 @@ function PaymentViaLicenseForm({
<Form>
<div className={CLASSES.DIALOG_BODY}>
<p>
<T id={'Pleasse enter your voucher number that you received from reseller.'} />
<T id={'payment_via_voucher.dialog.description'} />
</p>
<FastField name="license_code">

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { Intent } from '@blueprintjs/core';
import intl from 'react-intl-universal';
import { useHistory } from 'react-router-dom';
import AppToaster from 'components/AppToaster';
import withGlobalErrors from './withGlobalErrors';
@@ -17,29 +19,31 @@ function GlobalErrors({
globalErrorsSet,
}) {
if (globalErrors.something_wrong) {
toastKeySessionExpired = AppToaster.show({
toastKeySessionExpired = AppToaster.show(
{
message: intl.get('ops_something_went_wrong'),
intent: Intent.DANGER,
onDismiss: () => {
globalErrorsSet({ something_wrong: false });
}
}, toastKeySessionExpired);
},
},
toastKeySessionExpired,
);
}
if (globalErrors.session_expired) {
toastKeySomethingWrong = AppToaster.show({
toastKeySomethingWrong = AppToaster.show(
{
message: intl.get('session_expired'),
intent: Intent.DANGER,
onDismiss: () => {
globalErrorsSet({ session_expired: false });
},
},
toastKeySomethingWrong,
);
}
}, toastKeySomethingWrong);
}
return null;
}
export default compose(
withGlobalErrors,
withGlobalErrorsActions,
)(GlobalErrors);
export default compose(withGlobalErrors, withGlobalErrorsActions)(GlobalErrors);

View File

@@ -1,13 +1,15 @@
import React, { useCallback } from 'react';
import { Button, Intent } from '@blueprintjs/core';
import { useHistory } from 'react-router-dom';
import WorkflowIcon from './WorkflowIcon';
import { FormattedMessage as T } from 'components';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import { compose } from 'utils';
import 'style/pages/Setup/Congrats.scss';
import { compose } from 'utils';
/**
* Setup congrats page.
@@ -28,15 +30,15 @@ function SetupCongratsPage({ setOrganizationSetupCompleted }) {
<div class="setup-congrats__text">
<h1>
<T id={'congrats_you_are_ready_to_go'} />
<T id={'setup.congrats.title'} />
</h1>
<p class="paragraph">
<T id={'it_is_a_long_established_fact_that_a_reader'} />
<T id={'setup.congrats.description'} />
</p>
<Button intent={Intent.PRIMARY} type="submit" onClick={handleBtnClick}>
<T id={'go_to_dashboard'} />
<T id={'setup.congrats.go_to_dashboard'} />
</Button>
</div>
</div>

View File

@@ -15,7 +15,7 @@ export default function SetupInitializingForm() {
isError,
} = useBuildTenant();
useEffect(() => {
React.useEffect(() => {
buildTenantMutate();
}, [buildTenantMutate]);
@@ -27,32 +27,32 @@ export default function SetupInitializingForm() {
{isLoading ? (
<>
<h1>
<T id={'it_s_time_to_make_your_accounting_really_simple'} />
<T id={'setup.initializing.title'} />
</h1>
<p className={'paragraph'}>
<T
id={
'while_we_set_up_your_account_please_remember_to_verify_your_account'
}
/>
<T id={'setup.initializing.description'} />
</p>
</>
) : isError ? (
<>
<h1>
<T id={'something_went_wrong'} />
<T id={'setup.initializing.something_went_wrong'} />
</h1>
<p class="paragraph">
<T id={'please_refresh_the_page'} />
<T id={'setup.initializing.please_refresh_the_page'} />
</p>
</>
) : (
<>
<h1>
<T id={'waiting_to_redirect'} />
<T id={'setup.initializing.waiting_to_redirect'} />
</h1>
<p class="paragraph">
<T id={'refresh_the_page_if_redirect_not_worked'} />
<T
id={
'setup.initializing.refresh_the_page_if_redirect_not_worked'
}
/>
</p>
</>
)}

View File

@@ -3,11 +3,14 @@ import { Icon, For } from 'components';
import { FormattedMessage as T } from 'components';
import { getFooterLinks } from 'config/footerLinks';
import { useAuthActions, useAuthOrganizationId } from 'hooks/state';
import { useAuthActions } from 'hooks/state';
/**
* Footer item link.
*/
function FooterLinkItem({ title, link }) {
return (
<div class="">
<div class="content__links-item">
<a href={link} target="_blank">
{title}
</a>
@@ -16,20 +19,65 @@ function FooterLinkItem({ title, link }) {
}
/**
* Wizard setup left section.
* Setup left section footer.
*/
export default function SetupLeftSection() {
const { setLogout } = useAuthActions();
const organizationId = useAuthOrganizationId();
function SetupLeftSectionFooter() {
// Retrieve the footer links.
const footerLinks = getFooterLinks();
return (
<div className={'content__footer'}>
<div className={'content__contact-info'}>
<p>
<T id={'setup.left_side.footer_help'} />{' '}
<span>{'+21892-738-1987'}</span>
</p>
</div>
<div className={'content__links'}>
<For render={FooterLinkItem} of={footerLinks} />
</div>
</div>
);
}
/**
* Setup left section header.
*/
function SetupLeftSectionHeader() {
const { setLogout } = useAuthActions();
// Handle logout link click.
const onClickLogout = () => {
setLogout();
};
return (
<div className={'content__header'}>
<h1 className={'content__title'}>
<T id={'setup.left_side.title'} />
</h1>
<p className={'content__text'}>
<T id={'setup.left_side.description'} />
</p>
<div class="content__divider"></div>
<div className={'content__organization'}>
<span class="signout">
<a onClick={onClickLogout} href="#">
<T id={'sign_out'} />
</a>
</span>
</div>
</div>
);
}
/**
* Wizard setup left section.
*/
export default function SetupLeftSection() {
return (
<section className={'setup-page__left-section'}>
<div className={'content'}>
@@ -41,40 +89,8 @@ export default function SetupLeftSection() {
width={190}
/>
</div>
<h1 className={'content__title'}>
<T id={'register_a_new_organization_now'} />
</h1>
<p className={'content__text'}>
<T id={'you_have_a_bigcapital_account'} />
</p>
<span class="content__divider"></span>
<div className={'content__organization'}>
<span class="organization-id">
<T id={'organization_id'} />:{' '}
<span class="id">{organizationId}</span>,
</span>
<br />
<span class="signout">
<a onClick={onClickLogout} href="#">
<T id={'sign_out'} />
</a>
</span>
</div>
<div className={'content__footer'}>
<div className={'content__contact-info'}>
<p>
<T id={'we_re_here_to_help'} /> <span>{'+21892-738-1987'}</span>
</p>
</div>
<div className={'content__links'}>
<For render={FooterLinkItem} of={footerLinks} />
</div>
</div>
<SetupLeftSectionHeader />
<SetupLeftSectionFooter />
</div>
</section>
);

View File

@@ -0,0 +1,15 @@
import * as Yup from 'yup';
import intl from 'react-intl-universal';
// Retrieve the setup organization form validation.
export const getSetupOrganizationValidation = () =>
Yup.object().shape({
organization_name: Yup.string()
.required()
.label(intl.get('organization_name_')),
financialDateStart: Yup.date().required().label(intl.get('date_start_')),
baseCurrency: Yup.string().required().label(intl.get('base_currency_')),
language: Yup.string().required().label(intl.get('language')),
fiscalYear: Yup.string().required().label(intl.get('fiscal_year_')),
timeZone: Yup.string().required().label(intl.get('time_zone_')),
});

View File

@@ -14,20 +14,18 @@ import classNames from 'classnames';
import { TimezonePicker } from '@blueprintjs/timezone';
import { FormattedMessage as T } from 'components';
import { FieldRequiredHint, Col, Row, ListSelect } from 'components';
import { Col, Row, ListSelect } from 'components';
import {
momentFormatter,
tansformDateValue,
inputIntent,
handleDateChange
handleDateChange,
} from 'utils';
import { getFiscalYear } from 'common/fiscalYearOptions';
import { getLanguages } from 'common/languagesOptions';
import { getCurrencies } from 'common/currencies';
/**
* Setup organization form.
*/
@@ -46,22 +44,24 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
<FastField name={'organization_name'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'legal_organization_name'} />}
className={'form-group--name'}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name={'organization_name'} />}
>
<InputGroup {...field} />
<InputGroup {...field} intent={inputIntent({ error, touched })} />
</FormGroup>
)}
</FastField>
{/* ---------- Financial starting date ---------- */}
<FastField name={'financialDateStart'}>
{({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => (
{({
form: { setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'financial_starting_date'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="financialDateStart" />}
@@ -74,6 +74,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
onChange={handleDateChange((formattedDate) => {
setFieldValue('financialDateStart', formattedDate);
})}
intent={inputIntent({ error, touched })}
/>
</FormGroup>
)}
@@ -89,7 +90,6 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'base_currency'} />}
className={classNames(
'form-group--base-currency',
@@ -101,7 +101,9 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
>
<ListSelect
items={Currencies}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
noResults={
<MenuItem disabled={true} text={<T id={'no_results'} />} />
}
popoverProps={{ minimal: true }}
onItemSelect={(item) => {
setFieldValue('baseCurrency', item.code);
@@ -110,6 +112,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
textProp={'name'}
defaultText={<T id={'select_base_currency'} />}
selectedItem={value}
intent={inputIntent({ error, touched })}
/>
</FormGroup>
)}
@@ -136,7 +139,9 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
>
<ListSelect
items={Languages}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
noResults={
<MenuItem disabled={true} text={<T id={'no_results'} />} />
}
onItemSelect={(item) => {
setFieldValue('language', item.value);
}}
@@ -146,6 +151,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
defaultText={<T id={'select_language'} />}
popoverProps={{ minimal: true }}
filterable={false}
intent={inputIntent({ error, touched })}
/>
</FormGroup>
)}
@@ -154,9 +160,12 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
</Row>
{/* --------- Fiscal Year ----------- */}
<FastField name={'fiscalYear'}>
{({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => (
{({
form: { setFieldValue },
field: { value },
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'fiscal_year'} />}
className={classNames(
'form-group--fiscal_year',
@@ -168,14 +177,16 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
>
<ListSelect
items={FiscalYear}
noResults={<MenuItem disabled={true} text={<T id={'no_results'} />} />}
noResults={
<MenuItem disabled={true} text={<T id={'no_results'} />} />
}
selectedItem={value}
selectedItemProp={'value'}
textProp={'name'}
defaultText={<T id={'select_fiscal_year'} />}
popoverProps={{ minimal: true }}
onItemSelect={(item) => {
setFieldValue('fiscalYear', item.value)
setFieldValue('fiscalYear', item.value);
}}
filterable={false}
/>
@@ -191,7 +202,6 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'time_zone'} />}
className={classNames(
'form-group--time-zone',
@@ -216,20 +226,11 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
</FastField>
<p className={'register-org-note'}>
<T
id={
'note_you_can_change_your_preferences_later_in_dashboard_if_needed'
}
/>
<T id={'setup.organization.note_you_can_change_your_preferences'} />
</p>
<div className={'register-org-button'}>
<Button
intent={Intent.PRIMARY}
disabled={isSubmitting}
loading={isSubmitting}
type="submit"
>
<Button intent={Intent.PRIMARY} loading={isSubmitting} type="submit">
<T id={'save_continue'} />
</Button>
</div>

View File

@@ -1,9 +1,8 @@
import React from 'react';
import * as Yup from 'yup';
import { Formik } from 'formik';
import moment from 'moment';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import 'style/pages/Setup/Organization.scss';
@@ -13,42 +12,9 @@ import { useOrganizationSetup } from 'hooks/query';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import {
compose,
transfromToSnakeCase,
} from 'utils';
import { compose, transfromToSnakeCase } from 'utils';
import { getSetupOrganizationValidation } from './SetupOrganization.schema';
/**
* Setup organization form.
*/
function SetupOrganizationPage({
wizard,
setOrganizationSetupCompleted,
}) {
const { mutateAsync: organizationSetupMutate } = useOrganizationSetup();
// Validation schema.
const validationSchema = Yup.object().shape({
organization_name: Yup.string()
.required()
.label(intl.get('organization_name_')),
financialDateStart: Yup.date()
.required()
.label(intl.get('date_start_')),
baseCurrency: Yup.string()
.required()
.label(intl.get('base_currency_')),
language: Yup.string()
.required()
.label(intl.get('language')),
fiscalYear: Yup.string()
.required()
.label(intl.get('fiscal_year_')),
timeZone: Yup.string()
.required()
.label(intl.get('time_zone_')),
});
// Initial values.
const defaultValues = {
@@ -60,6 +26,16 @@ function SetupOrganizationPage({
timeZone: '',
};
/**
* Setup organization form.
*/
function SetupOrganizationPage({ wizard, setOrganizationSetupCompleted }) {
const { mutateAsync: organizationSetupMutate } = useOrganizationSetup();
// Validation schema.
const validationSchema = getSetupOrganizationValidation();
// Initialize values.
const initialValues = {
...defaultValues,
};
@@ -83,10 +59,10 @@ function SetupOrganizationPage({
<div className={'setup-organization'}>
<div className={'setup-organization__title-wrap'}>
<h1>
<T id={'let_s_get_started'} />
<T id={'setup.organization.title'} />
</h1>
<p class="paragraph">
<T id={'tell_the_system_a_little_bit_about_your_organization'} />
<T id={'setup.organization.description'} />
</p>
</div>

View File

@@ -4,7 +4,7 @@ import * as R from 'ramda';
import 'style/pages/Setup/Subscription.scss';
import SetupSubscriptionForm from './SetupSubscriptionForm';
import SetupSubscriptionForm from './SetupSubscription/SetupSubscriptionForm';
import { getSubscriptionFormSchema } from './SubscriptionForm.schema';
import withSubscriptionPlansActions from '../Subscriptions/withSubscriptionPlansActions';
@@ -13,13 +13,11 @@ import withSubscriptionPlansActions from '../Subscriptions/withSubscriptionPlans
*/
function SetupSubscription({
// #withSubscriptionPlansActions
initSubscriptionPlans
initSubscriptionPlans,
}) {
React.useEffect(() => {
initSubscriptionPlans();
}, [
initSubscriptionPlans
]);
}, [initSubscriptionPlans]);
// Initial values.
const initialValues = {
@@ -30,7 +28,11 @@ function SetupSubscription({
// Handle form submit.
const handleSubmit = () => {};
const SubscriptionFormSchema = getSubscriptionFormSchema();
// Retrieve momerized subscription form schema.
const SubscriptionFormSchema = React.useMemo(
() => getSubscriptionFormSchema(),
[],
);
return (
<div className={'setup-subscription-form'}>
@@ -44,6 +46,4 @@ function SetupSubscription({
);
}
export default R.compose(
withSubscriptionPlansActions,
)(SetupSubscription);
export default R.compose(withSubscriptionPlansActions)(SetupSubscription);

View File

@@ -0,0 +1,16 @@
import React from 'react';
import SubscriptionPlansSection from './SubscriptionPlansSection';
import SubscriptionPeriodsSection from './SubscriptionPeriodsSection';
import SubscriptionPaymentMethodsSection from './SubscriptionPaymentsMethodsSection';
export default function SetupSubscriptionForm() {
return (
<div class="billing-plans">
<SubscriptionPlansSection />
<SubscriptionPeriodsSection />
<SubscriptionPaymentMethodsSection />
</div>
);
}

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { T } from 'components';
import { PaymentMethodTabs } from '../../Subscriptions/SubscriptionTabs';
export default ({ formik, title, description }) => {
return (
<section class="billing-plans__section">
<h1 className="title">
<T id={'setup.plans.payment_methods.title'} />
</h1>
<p className="paragraph">
<T id={'setup.plans.payment_methods.description'} />
</p>
<PaymentMethodTabs formik={formik} />
</section>
);
};

View File

@@ -0,0 +1,43 @@
import React from 'react';
import { Field } from 'formik';
import * as R from 'ramda';
import { T, SubscriptionPeriods } from 'components';
import withPlan from '../../Subscriptions/withPlan';
const SubscriptionPeriodsEnhanced = R.compose(
withPlan(({ plan }) => ({ plan })),
)(({ plan, ...restProps }) => {
return <SubscriptionPeriods periods={plan.periods} {...restProps} />;
});
/**
* Billing periods.
*/
export default function SubscriptionPeriodsSection() {
return (
<section class="billing-plans__section">
<h1 class="title">
<T id={'setup.plans.select_period.title'} />
</h1>
<div class="description">
<p className="paragraph">
<T id={'setup.plans.select_period.description'} />
</p>
</div>
<Field name={'period'}>
{({ form: { setFieldValue, values }, field: { value } }) => (
<SubscriptionPeriodsEnhanced
planSlug={values.plan_slug}
selectedPeriod={value}
onPeriodSelect={(period) => {
setFieldValue('period', period);
}}
/>
)}
</Field>
</section>
);
}

View File

@@ -0,0 +1,41 @@
import React from 'react';
import { Field } from 'formik';
import { T, SubscriptionPlans } from 'components';
import { compose } from 'utils';
import withPlans from '../../Subscriptions/withPlans';
/**
* Billing plans.
*/
function SubscriptionPlansSection({ plans }) {
return (
<section class="billing-plans__section">
<h1 class="title">
<T id={'setup.plans.select_plan.title'} />
</h1>
<div class="description">
<p className="paragraph">
<T id={'setup.plans.select_plan.description'} />
</p>
</div>
<Field name={'plan_slug'}>
{({ form: { setFieldValue }, field: { value } }) => (
<SubscriptionPlans
value={value}
plans={plans}
onSelect={(value) => {
setFieldValue('plan_slug', value);
}}
/>
)}
</Field>
</section>
);
}
export default compose(withPlans(({ plans }) => ({ plans })))(
SubscriptionPlansSection,
);

View File

@@ -1,15 +0,0 @@
import React from 'react';
import { Form } from 'formik';
import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm';
/**
* Subscription step of wizard setup.
*/
export default function SetupSubscriptionForm() {
return (
<Form>
<BillingPlansForm />
</Form>
);
}

View File

@@ -5,9 +5,7 @@ import { getSetupWizardSteps } from 'common/registerWizard';
function WizardSetupStep({ label, isActive = false }) {
return (
<li className={classNames({ 'is-active': isActive })}>
<p className={'wizard-info'}>
{ label }
</p>
<p className={'wizard-info'}>{label}</p>
</li>
);
}

View File

@@ -1,7 +1,8 @@
import React, { useEffect } from 'react';
import * as Yup from 'yup';
import { Formik, Form } from 'formik';
import intl from 'react-intl-universal';
import { If, Alert, T } from 'components';
import DashboardInsider from 'components/Dashboard/DashboardInsider';
import 'style/pages/Billing/BillingPage.scss';
@@ -10,8 +11,11 @@ import { MasterBillingTabs } from './SubscriptionTabs';
import withBillingActions from './withBillingActions';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withSubscriptionPlansActions from './withSubscriptionPlansActions';
import { compose } from 'utils';
import { getBillingFormValidationSchema } from './utils';
import withSubscriptions from './withSubscriptions';
/**
* Billing form.
@@ -22,24 +26,28 @@ function BillingForm({
// #withBillingActions
requestSubmitBilling,
initSubscriptionPlans,
// #withSubscriptions
isSubscriptionInactive,
}) {
useEffect(() => {
changePageTitle(intl.get('billing'));
}, [changePageTitle]);
const validationSchema = Yup.object().shape({
plan_slug: Yup.string()
.required(),
period: Yup.string().required(),
license_code: Yup.string().trim(),
});
React.useEffect(() => {
initSubscriptionPlans();
}, [initSubscriptionPlans]);
// Initial values.
const initialValues = {
plan_slug: 'free',
period: 'month',
license_code: '',
};
// Handle form submitting.
const handleSubmit = (values, { setSubmitting }) => {
requestSubmitBilling(values)
.then((response) => {
@@ -53,20 +61,34 @@ function BillingForm({
return (
<DashboardInsider name={'billing-page'}>
<div className={'billing-page'}>
<If condition={isSubscriptionInactive}>
<Alert
intent={'danger'}
title={<T id={'billing.suspend_message.title'} />}
description={<T id={'billing.suspend_message.description'} />}
/>
</If>
<Formik
validationSchema={validationSchema}
validationSchema={getBillingFormValidationSchema()}
onSubmit={handleSubmit}
initialValues={initialValues}
>
{({ isSubmitting, handleSubmit }) => (
<Form>
<MasterBillingTabs />
</Form>
)}
</Formik>
</div>
</DashboardInsider>
);
}
export default compose(withDashboardActions, withBillingActions)(BillingForm);
export default compose(
withDashboardActions,
withBillingActions,
withSubscriptionPlansActions,
withSubscriptions(
({ isSubscriptionInactive }) => ({ isSubscriptionInactive }),
'main',
),
)(BillingForm);

View File

@@ -15,12 +15,14 @@ import { saveInvoke } from 'utils';
function BillingPeriod({
// #ownProps
label,
currencyCode,
value,
selectedOption,
onSelected,
period,
// #withPlan
price,
currencyCode,
}) {
const handlePeriodClick = () => {
saveInvoke(onSelected, value);

View File

@@ -1,42 +1,46 @@
import React from 'react';
import { Field } from 'formik';
import BillingPeriod from './BillingPeriod';
import * as R from 'ramda';
import withPlans from './withPlans';
import { T, SubscriptionPeriods } from 'components';
import { compose } from 'utils';
import withPlan from './withPlan';
/**
* Sunscription periods enhanced.
*/
const SubscriptionPeriodsEnhanced = R.compose(
withPlan(({ plan }) => ({ plan })),
)(({ plan, ...restProps }) => {
return <SubscriptionPeriods periods={plan.periods} {...restProps} />;
});
/**
* Billing periods.
*/
function BillingPeriods({ title, description, plansPeriods }) {
export default function BillingPeriods() {
return (
<section class="billing-plans__section">
<h1 class="title">{title}</h1>
<h1 class="title">
<T id={'setup.plans.select_period.title'} />
</h1>
<div class="description">
<p className="paragraph">{description}</p>
<p className="paragraph">
<T id={'setup.plans.select_period.description'} />
</p>
</div>
<Field name={'period'}>
{({ form: { setFieldValue, values } }) => (
<div className={'plan-periods'}>
{plansPeriods.map((period) => (
<BillingPeriod
{({ field: { value }, form: { values, setFieldValue } }) => (
<SubscriptionPeriodsEnhanced
selectedPeriod={value}
planSlug={values.plan_slug}
period={period.slug}
label={period.label}
value={period.slug}
onSelected={(value) => setFieldValue('period', value)}
selectedOption={values.period}
onPeriodSelect={(period) => {
setFieldValue('period', period);
}}
/>
))}
</div>
)}
</Field>
</section>
);
}
export default compose(withPlans(({ plansPeriods }) => ({ plansPeriods })))(
BillingPeriods,
);

View File

@@ -3,8 +3,6 @@ import classNames from 'classnames';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import 'style/pages/Subscription/PlanRadio.scss';
import { saveInvoke } from 'utils';
/**
@@ -32,9 +30,7 @@ export default function BillingPlan({
onClick={handlePlanClick}
>
<div className={'plan-radio__header'}>
<div className={'plan-radio__name'}>
{intl.get('plan_radio_name', { name: name })}
</div>
<div className={'plan-radio__name'}>{name}</div>
</div>
<div className={'plan-radio__description'}>

View File

@@ -1,12 +1,10 @@
import React from 'react';
import { FormattedMessage as T } from 'components';
import intl from 'react-intl-universal';
import 'style/pages/Subscription/BillingPlans.scss';
import BillingPlansInput from 'containers/Subscriptions/BillingPlansInput';
import BillingPeriodsInput from 'containers/Subscriptions/BillingPeriodsInput';
import BillingPaymentMethod from 'containers/Subscriptions/BillingPaymentMethod';
import BillingPlansInput from './BillingPlansInput';
import BillingPeriodsInput from './BillingPeriodsInput';
import BillingPaymentMethod from './BillingPaymentMethod';
/**
* Billing plans form.
@@ -14,18 +12,9 @@ import BillingPaymentMethod from 'containers/Subscriptions/BillingPaymentMethod'
export default function BillingPlansForm() {
return (
<div class="billing-plans">
<BillingPlansInput
title={intl.get('select_a_plan')}
description={<T id={'please_enter_your_preferred_payment_method'} />}
/>
<BillingPeriodsInput
title={intl.get('choose_your_billing')}
description={<T id={'please_enter_your_preferred_payment_method'} />}
/>
<BillingPaymentMethod
title={intl.get('payment_methods')}
description={<T id={'please_enter_your_preferred_payment_method'} />}
/>
<BillingPlansInput />
<BillingPeriodsInput />
<BillingPaymentMethod />
</div>
);
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { FastField, Field } from 'formik';
import BillingPlan from './BillingPlan';
import { Field } from 'formik';
import { T, SubscriptionPlans } from 'components';
import withPlans from './withPlans';
import { compose } from 'utils';
@@ -11,27 +11,24 @@ import { compose } from 'utils';
function BillingPlans({ plans, title, description, selectedOption }) {
return (
<section class="billing-plans__section">
<h1 class="title">{title}</h1>
<h1 class="title">
<T id={'setup.plans.select_plan.title'} />
</h1>
<div class="description">
<p className="paragraph">{description}</p>
<p className="paragraph">
<T id={'setup.plans.select_plan.description'} />
</p>
</div>
<Field 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}
<SubscriptionPlans
plans={plans}
value={value}
onSelect={(value) => {
setFieldValue('plan_slug', value);
}}
/>
))}
</div>
)}
</Field>
</section>

View File

@@ -1,10 +1,6 @@
import React from 'react';
import BillingPlansForm from 'containers/Subscriptions/BillingPlansForm';
import BillingPlansForm from './BillingPlansForm';
export default function BillingTab() {
return (
<div>
<BillingPlansForm />
</div>
);
return (<BillingPlansForm />);
}

View File

@@ -16,11 +16,7 @@ export const MasterBillingTabs = ({ formik }) => {
id={'billing'}
panel={<BillingTab formik={formik} />}
/>
<Tab
title={intl.get('usage')}
id={'usage'}
disabled={true}
/>
<Tab title={intl.get('usage')} id={'usage'} disabled={true} />
</Tabs>
</div>
);
@@ -43,11 +39,7 @@ export const PaymentMethodTabs = ({ formik }) => {
id={'credit_card'}
disabled={true}
/>
<Tab
title={intl.get('paypal')}
id={'paypal'}
disabled={true}
/>
<Tab title={intl.get('paypal')} id={'paypal'} disabled={true} />
</Tabs>
</div>
);

View File

@@ -1,11 +1,13 @@
import React from 'react';
import { T } from 'components';
import { PaymentMethodTabs } from './SubscriptionTabs';
export default ({ formik, title, description }) => {
return (
<section class="billing-plans__section">
<h1 className="title">{ title }</h1>
<p className="paragraph">{ description }</p>
<h1 className="title"><T id={'setup.plans.payment_methods.title'} /></h1>
<p className="paragraph"><T id={'setup.plans.payment_methods.description' } /></p>
<PaymentMethodTabs formik={formik} />
</section>

View File

@@ -0,0 +1,8 @@
import * as Yup from 'yup';
export const getBillingFormValidationSchema = () =>
Yup.object().shape({
plan_slug: Yup.string().required(),
period: Yup.string().required(),
license_code: Yup.string().trim(),
});

View File

@@ -1,17 +1,14 @@
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;
};

View File

@@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import {
isSubscriptionsInactiveFactory,
isSubscriptionsActiveFactory
} from 'store/subscription/subscription.selectors';
export default (mapState) => {
const isSubscriptionsInactive = isSubscriptionsInactiveFactory();
const isSubscriptionsActive = isSubscriptionsActiveFactory();
const mapStateToProps = (state, props) => {
const mapped = {
isSubscriptionsInactive: isSubscriptionsInactive(state, props),
isSubscriptionsActive: isSubscriptionsActive(state, props),
};
return (mapState) ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);
};

View File

@@ -23,23 +23,18 @@ export function useSaveSettings(props) {
function useSettingsQuery(key, query, props) {
const setSettings = useSetSettings();
const state = useRequestQuery(
return useRequestQuery(
key,
{ method: 'get', url: 'settings', params: query },
{
select: (res) => res.data.settings,
defaultData: [],
onSuccess: (settings) => {
setSettings(settings);
},
...props,
},
);
useEffect(() => {
if (state.isSuccess) {
setSettings(state.data);
}
}, [state.data, state.isSuccess, setSettings]);
return state.data;
}
/**

View File

@@ -584,7 +584,7 @@
"payment_mades": "سندات الموردين",
"subscription": "الاشتراك",
"plan_slug": "سبيكة خطة",
"billing": "الفواتير",
"billing": "الاشتراك",
"the_billing_has_been_created_successfully": "تم إنشاء الفواتير بنجاح.",
"select_a_plan": "حدد الباقة",
"choose_your_billing": "اختر مدة الدفع",
@@ -1114,12 +1114,10 @@
"filter_": "البحث...",
"all_rights_reserved": "© {pre}-{current} كل الحقوق محفوظة.",
"congrats_your_account_has_been_created_and_invited": "مبروك! تم إنشاء حسابك ودعوتك إلى <strong>{organization_name} </strong> بنجاح.",
"it_s_time_to_make_your_accounting_really_simple": "حان الوقت لجعل عملية المحاسبة الخاصة بك بسيطة واكتر دقة!",
"while_we_set_up_your_account_please_remember_to_verify_your_account": "أثناء قيامنا بإعداد حسابك ، يرجى تذكر التحقق من حسابك بالنقر فوق الارتباط الذي أرسلناه إلى عنوان بريدك الإلكتروني المسجل",
"something_went_wrong": "هناك خطأ ما!",
"please_refresh_the_page": "يرجى تحديث الصفحة",
"waiting_to_redirect": "في انتظار إعادة التوجيه",
"refresh_the_page_if_redirect_not_worked": "قم بتحديث الصفحة إذا لم تنجح إعادة التوجيه.",
"blog": "المدونة",
"support": "الدعم النفي",
"service_status": "حالة الخدمة",
@@ -1220,6 +1218,67 @@
"pdf_preview.preview.button": "معاينة",
"invoice_preview.dialog.title": "معاينة فاتورة PDF",
"estimate_preview.dialog.title": "معاينة عرض PDF",
"receipt_preview.dialog.title":"معاينة إيصال PDF"
"receipt_preview.dialog.title":"معاينة إيصال PDF",
"edit_contact": "Edit {name}",
"item.sell_description": "Sell description",
"item.purchase_description": "Purchase description",
"closed_date": "Closed date",
"payment_made.details.payment_number": "Payment #",
"payment_receive.details.payment_number": "Payment #",
"estimate.details.estimate_number": "Estimate #",
"receipt.details.receipt_number": "Receipt #",
"bill.details.bill_number": "Bill #",
"setup.left_side.title": "سجل منشأة جديدة الأن.",
"setup.left_side.description": "حسابك علي Bigcapital",
"setup.left_side.footer_help": "نحن هنا للمساعدة!",
"setup.plan.plans": "الخطط والدفع",
"setup.plan.initializing": "التهيئة",
"setup.plan.getting_started": "البدء",
"setup.plan.congrats": "تهانينا",
"setup.plans.select_plan.description": "الرجاء إدخال طريقة الدفع المفضلة لديك أدناه. يمكنك استخدام بطاقة الائتمان / الخصم أو الدفع المسبق من خلال PayPal. ",
"setup.plans.select_plan.title": "اختر الخطة",
"setup.plans.select_period.title": "اختر الفترة",
"setup.plans.select_period.description": "يمكن تسديد الخطة شهرياً او سنوياً.",
"setup.plans.payment_methods.title": "طرق الدفع",
"setup.plans.payment_methods.description": "",
"setup.initializing.title": "حان الوقت لجعل عملية المحاسبة الخاصة بك بسيطة واكتر دقة!",
"setup.initializing.description": "أثناء قيامنا بإعداد حسابك ، يرجى تذكر التحقق من حسابك بالنقر فوق الارتباط الذي أرسلناه إلى عنوان بريدك الإلكتروني المسجل",
"setup.initializing.waiting_to_redirect": "في انتظار إعادة التوجيه",
"setup.initializing.refresh_the_page_if_redirect_not_worked": "قم بتحديث الصفحة إذا لم تنجح إعادة التوجيه.",
"setup.initializing.something_went_wrong": "هناك خطأ ما!",
"setup.initializing.please_refresh_the_page": "يرجى تحديث الصفحة",
"setup.organization.title": "دعنا نبدأ",
"setup.organization.description": "أخبر النظام قليلاً عن مؤسستك.",
"plan.essential.title": "الاساسية",
"plan.plus.title": "الاضافية",
"plan.professional.title": "الاحترافية",
"plan.feature.sale_purchase_invoice": "فواتير البيع والشراء.",
"plan.feature.receivable_payable_accounts": "حسابات العملاء والموردين.",
"plan.feature.expenses_tracking": "تتبع المصروفات",
"plan.feature.manual_journal": "القيود اليدوية",
"plan.feature.financial_reports": "القوائم المالية",
"plan.feature.one_user_with_accountant": "لمستخدم واحد والمحاسب",
"plan.feature.all_capital_essential": "جميع مميزات الباقة الاساسية",
"plan.feature.multi_currency": "تعدد العملات",
"plan.feature.purchase_sell_orders": "أوامر الشراء والبيع.",
"plan.feature.multi_inventory_managment": "تعدد المخازن.",
"plan.feature.three_users": "ثلاثة مستخدمين مع المحاسب",
"plan.feature.advanced_financial_reports": "تقارير المالية المتقدمة",
"plan.feature.tracking_multi_locations": "تتبع الفروع والمواقع",
"plan.feture.all_capital_professional_features": "جميع مميزات الباقة الاحترافية",
"plan.feature.projects_accounting": "محاسبة المشاريع والجداول الزمنية",
"plan.feature.accounting_dimensions": "محاسبة ثلاثية الابعاد",
"plan.monthly": "شهريا",
"plan.yearly": "سنوياً",
"payment_via_voucher.success_message": "تم الدفع وتجديد واشتراكك بنجاح.",
"payment_via_voucher.license_code_not_valid": "رقم الرخصة غير صالح ، يرجي المحاولة مرة أخرى",
"payment_via_voucher.dialog.description": "الرجاء إدخال رقم الرخصة التي استلمتها عند تجديد او طلب الاشتراك .",
"setup.organization.note_you_can_change_your_preferences": "ملاحظة: يمكنك تغيير تفضيلاتك لاحقًا في لوحة التحكم ، إذا لزم الأمر.",
"setup.congrats.title": "تهانينا! حسابك جاهز للعمل",
"setup.congrats.description": "تهانينا ، تم إعداد مؤسستك بنجاح. يمكنك البدء في العمل بمجرد الانتقال إلى لوحة التحكم.",
"setup.congrats.go_to_dashboard": "اذهب إلي لوحة التحكم",
"billing.suspend_message.title": "حسابك معلق :(",
"billing.suspend_message.description": "تم تعليق حسابك بسبب انتهاء فترة الاشتراك. الرجاء تجديد الاشتراك لتفعيل الحساب.",
"dashboard.subscription_msg.period_over": "انتهت فترة الاشتراك"
}

View File

@@ -578,7 +578,6 @@
"plan_slug": "Plan slug",
"billing": "Billing",
"the_billing_has_been_created_successfully": "The billing has been created successfully.",
"select_a_plan": "Select a plan",
"choose_your_billing": "Choose your billing",
"payment_methods": "Payment methods",
"usage": "Usage",
@@ -591,7 +590,6 @@
"yearly": "Yearly",
"license_code": "License Code",
"year": "Year",
"please_enter_your_preferred_payment_method": "Please enter your preferred payment method below. You can use a credit / debit card or prepay through PayPal. ",
"cards_will_be_charged": "Cards will be charged either at the end of the month or whenever your balance exceeds the usage threshold. All major credit / debit cards accepted.",
"license_number": "License number",
"subscribe": "Subscribe",
@@ -616,20 +614,15 @@
"purchasable": "Purchasable",
"sell_account": "Sell Account",
"cost_account": "Cost Account",
"register_a_new_organization_now": "Register a New Organization now!.",
"you_have_a_bigcapital_account": "You have a Bigcapital account ",
"contact_us_technical_support": "Contact us - Technical Support",
"let_s_get_started": "Lets Get Started",
"tell_the_system_a_little_bit_about_your_organization": "Tell the system a little bit about your organization.",
"organization_details": "Organization details",
"financial_starting_date": "Financial starting date ",
"note_you_can_change_your_preferences_later_in_dashboard_if_needed": "Note: You can change your preferences later in dashboard, if needed.",
"save_continue": "Save & Continue",
"organization_register": "Organization Register",
"fiscal_year_": "Fiscal year",
"welcome": "Welcome ",
"sign_out": "Sign out",
"we_re_here_to_help": "Were Here to Help!",
"date_start_": "Date start",
"something_wentwrong": "Something went wrong.",
"license_code_": "License code",
@@ -906,7 +899,6 @@
"accounting": "Accounting",
"system": "SYSTEM",
"it_s_time_to_send_estimates_to_your_customers": "It's time to send estimates to your customers",
"it_is_a_long_established_fact_that_a_reader": "It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.",
"new_sale_estimate": "New sale estimate",
"learn_more": "Learn more",
"back_to_list": "Back to list.",
@@ -1068,8 +1060,7 @@
"products_you_buy_and_or_sell": "<strong> Inventory :</strong> Products you buy and/or sell and that you track quantities of.",
"products_you_buy_and_or_sell_but_don_t_need": "<strong> Non-Inventory:</strong> Products you buy and/or sell but dont need to (or cant) track quantities of, for example, nuts and bolts used in an installation.",
"there_is_no_items_in_the_table_yet": "There is no items in the table yet.",
"congrats_you_are_ready_to_go": "Congrats! You are ready to go",
"go_to_dashboard": "Go to dashboard",
"mr": "Mr.",
"mrs": "Mrs.",
"ms": "Ms.",
@@ -1101,25 +1092,13 @@
"filter_": "Filter...",
"all_rights_reserved": "© {pre}-{current} All Rights Reserved.",
"congrats_your_account_has_been_created_and_invited": "Congrats! Your account has been created and invited to <strong>{organization_name} </strong> organization successfully.",
"it_s_time_to_make_your_accounting_really_simple": "It's time to make your accounting really simple!",
"while_we_set_up_your_account_please_remember_to_verify_your_account": "while we set up your account, please remember to verify your account by clicking on the link we sent to your registered email address",
"something_went_wrong": "Something went wrong!",
"please_refresh_the_page": "Please refresh the page",
"waiting_to_redirect": "Waiting to redirect",
"refresh_the_page_if_redirect_not_worked": "Refresh the page if redirect not worked.",
"blog": "Blog",
"support": "Support",
"service_status": "Service Status",
"pricing": "Pricing",
"reseller_partner": "Reseller Partner",
"docs": "Docs",
"Pleasse enter your voucher number that you received from reseller.": "Pleasse enter your voucher number that you received from reseller.",
"Sale and purchase invoices.": "Sale and purchase invoices.",
"Customers/vendors accounts.": "Customers/vendors accounts.",
"Expenses tracking.": "Expenses tracking.",
"Manual journals.": "Manual journals.",
"Financial reports.": "Financial reports.",
"All Capital Starter features.": "All Capital Starter features.",
"Multi-currency.": "Multi-currency.",
"Purchase and sell orders.": "Purchase and sell orders.",
"Inventory management.": "Inventory management.",
@@ -1129,14 +1108,13 @@
"Track multi-branches and locations.": "Track multi-branches and locations.",
"Projects accounting and timesheets.": "Projects accounting and timesheets.",
"Accounting dimensions.": "Accounting dimensions.",
"For one user and accountant.": "For one user and accountant.",
"Monthly": "Monthly",
"Yearly": "Yearly",
"Plans & Payment": "Plans & Payment",
"Initializing": "Initializing",
"Getting started": "Getting started",
"Congratulations": "Congratulations",
"payment_has_been_done_successfully": "Payment has been done successfully.",
"manual_journal_number": "Manual journal {number}",
"conditions_and_terms": "Conditions and terms",
"allocate_landed_coast": "Allocate landed cost",
@@ -1217,5 +1195,57 @@
"payment_receive.details.payment_number": "Payment #",
"estimate.details.estimate_number": "Estimate #",
"receipt.details.receipt_number": "Receipt #",
"bill.details.bill_number": "Bill #"
"bill.details.bill_number": "Bill #",
"setup.left_side.title": "Register a New Organization now!.",
"setup.left_side.description": "You have a Bigcapital account",
"setup.left_side.footer_help": "Were Here to Help!",
"setup.plan.plans": "Plans & Payment",
"setup.plan.initializing": "Initializing",
"setup.plan.getting_started": "Getting started",
"setup.plan.congrats": "Congratulations",
"setup.plans.select_plan.description": "Please enter your preferred payment method below. You can use a credit / debit card or prepay through PayPal. ",
"setup.plans.select_plan.title": "Select a plan",
"setup.plans.select_period.title": "Plan period",
"setup.plans.select_period.description": "The plan could be billed monthly or annually.",
"setup.plans.payment_methods.title": "Payment methods",
"setup.plans.payment_methods.description": "",
"setup.initializing.title": "It's time to make your accounting really simple!",
"setup.initializing.description": "while we set up your account, please remember to verify your account by clicking on the link we sent to your registered email address",
"setup.initializing.waiting_to_redirect": "Waiting to redirect",
"setup.initializing.refresh_the_page_if_redirect_not_worked": "Refresh the page if redirect not worked.",
"setup.initializing.something_went_wrong": "Something went wrong!",
"setup.initializing.please_refresh_the_page": "Please refresh the page",
"setup.organization.title": "Lets Get Started",
"setup.organization.description": "Tell the system a little bit about your organization.",
"plan.professional.title": "Pro",
"plan.essential.title": "Essential",
"plan.plus.title": "Plus+",
"plan.feature.sale_purchase_invoice": "Sale and purchase invoices.",
"plan.feature.receivable_payable_accounts": "Customers/vendors accounts.",
"plan.feature.expenses_tracking": "Expenses tracking",
"plan.feature.manual_journal": "Manual journals.",
"plan.feature.financial_reports": "Financial reports.",
"plan.feature.one_user_with_accountant": "For one user and accountant",
"plan.feture.all_capital_professional_features": "All Capital Pro features.",
"plan.feature.multi_currency": "Multi-currency.",
"plan.feature.purchase_sell_orders": "Purchase and sell orders.",
"plan.feature.multi_inventory_managment": "Mutli-inventory managment.",
"plan.feature.three_users": "Three users with your accountant",
"plan.feature.advanced_financial_reports": "Advanced financial reports",
"plan.feature.tracking_multi_locations": "Track multi-branches and locations",
"plan.feature.all_capital_essential": "All Capital Essential features.",
"plan.feature.projects_accounting": "Projects accounting and timesheets",
"plan.feature.accounting_dimensions": "Accounting dimensions.",
"plan.monthly": "Monthly",
"plan.yearly": "Yearly",
"payment_via_voucher.success_message": "Payment has been done successfully.",
"payment_via_voucher.license_code_not_valid": "The license code is not valid, please try agin.",
"payment_via_voucher.dialog.description": "Pleasse enter your voucher number that you received from reseller.",
"setup.organization.note_you_can_change_your_preferences": "Note: You can change your preferences later in dashboard, if needed.",
"setup.congrats.title": "Congrats! You are ready to go",
"setup.congrats.description": "Congrats, Your organization is set up successfully. you can start working once go to dashboard.",
"setup.congrats.go_to_dashboard": "Go to dashboard",
"billing.suspend_message.title": "Your account is suspended :(",
"billing.suspend_message.description": "Your account has been suspended due to the expiration of the subscription period. Please renew the subscription to activate the account.",
"dashboard.subscription_msg.period_over": "Subscription period is over"
}

View File

@@ -3,10 +3,14 @@ import { lazy } from 'react';
import intl from 'react-intl-universal';
import { RESOURCES_TYPES } from '../common/resourcesTypes';
const SUBSCRIPTION_TYPE = {
MAIN: 'main',
}
// const BASE_URL = '/dashboard';
export const getDashboardRoutes = () => [
// // Accounts.
// Accounts.
{
path: `/accounts`,
component: lazy(() => import('containers/Accounts/AccountsChart')),
@@ -14,6 +18,7 @@ export const getDashboardRoutes = () => [
hotkey: 'shift+a',
pageTitle: intl.get('accounts_chart'),
defaultSearchResource: RESOURCES_TYPES.ACCOUNT,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Accounting.
{
@@ -27,6 +32,7 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/manual-journals/:id/edit`,
@@ -38,6 +44,7 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/manual-journals`,
@@ -48,6 +55,7 @@ export const getDashboardRoutes = () => [
hotkey: 'shift+m',
pageTitle: intl.get('manual_journals'),
defaultSearchResource: RESOURCES_TYPES.MANUAL_JOURNAL,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/items/categories`,
@@ -57,6 +65,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('categories'),
pageTitle: intl.get('category_list'),
defaultSearchResource: RESOURCES_TYPES.ITEM,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Items.
{
@@ -67,6 +76,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_item'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.ITEM,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/items/new?duplicate=/:id`,
@@ -75,6 +85,7 @@ export const getDashboardRoutes = () => [
}),
breadcrumb: intl.get('duplicate_item'),
defaultSearchResource: RESOURCES_TYPES.ITEM,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/items/new`,
@@ -85,6 +96,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_item'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.ITEM,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/items`,
@@ -93,6 +105,7 @@ export const getDashboardRoutes = () => [
hotkey: 'shift+w',
pageTitle: intl.get('items_list'),
defaultSearchResource: RESOURCES_TYPES.ITEM,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Inventory adjustments.
@@ -104,6 +117,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('inventory_adjustments'),
pageTitle: intl.get('inventory_adjustment_list'),
defaultSearchResource: RESOURCES_TYPES.ITEM,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Financial Reports.
@@ -119,6 +133,7 @@ export const getDashboardRoutes = () => [
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.INVENTORY_ADJUSTMENT,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/balance-sheet`,
@@ -131,6 +146,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('balance_sheet'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/trial-balance-sheet`,
@@ -145,6 +161,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('trial_balance_sheet'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/profit-loss-sheet`,
@@ -157,6 +174,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('profit_loss_sheet'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: '/financial-reports/receivable-aging-summary',
@@ -168,6 +186,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('receivable_aging_summary'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: '/financial-reports/payable-aging-summary',
@@ -179,6 +198,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('payable_aging_summary'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/journal-sheet`,
@@ -191,6 +211,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('journal_sheet'),
sidebarExpand: false,
backLink: true,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/purchases-by-items`,
@@ -204,6 +225,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('purchases_by_items'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/sales-by-items`,
@@ -217,6 +239,7 @@ export const getDashboardRoutes = () => [
),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/inventory-valuation`,
@@ -230,6 +253,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('inventory_valuation'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/customers-balance-summary`,
@@ -243,6 +267,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('customers_balance_summary'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/vendors-balance-summary`,
@@ -256,6 +281,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('vendors_balance_summary'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/transactions-by-customers`,
@@ -269,6 +295,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('customers_transactions'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/transactions-by-vendors`,
@@ -282,6 +309,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('vendors_transactions'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/cash-flow`,
@@ -295,6 +323,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('cash_flow_statement'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/financial-reports/inventory-item-details`,
@@ -308,6 +337,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('inventory_item_details'),
backLink: true,
sidebarExpand: false,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: '/financial-reports',
@@ -316,6 +346,7 @@ export const getDashboardRoutes = () => [
),
breadcrumb: intl.get('financial_reports'),
pageTitle: intl.get('all_financial_reports'),
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Exchange Rates
{
@@ -323,6 +354,7 @@ export const getDashboardRoutes = () => [
component: lazy(() => import('containers/ExchangeRates/ExchangeRatesList')),
breadcrumb: intl.get('exchange_rates_list'),
pageTitle: intl.get('exchange_rates_list'),
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Expenses.
{
@@ -336,6 +368,7 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.EXPENSE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/expenses/:id/edit`,
@@ -347,6 +380,7 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.EXPENSE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/expenses`,
@@ -357,6 +391,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('expenses_list'),
hotkey: 'shift+x',
defaultSearchResource: RESOURCES_TYPES.EXPENSE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Customers
@@ -370,6 +405,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_customer'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/customers/new`,
@@ -382,6 +418,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_customer'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/customers`,
@@ -392,6 +429,7 @@ export const getDashboardRoutes = () => [
hotkey: 'shift+c',
pageTitle: intl.get('customers_list'),
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/customers/contact_duplicate=/:id`,
@@ -403,6 +441,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_customer'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.CUSTOMER,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Vendors
@@ -416,6 +455,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('edit_vendor'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.VENDOR,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/vendors/new`,
@@ -428,6 +468,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_vendor'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.VENDOR,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/vendors`,
@@ -438,6 +479,7 @@ export const getDashboardRoutes = () => [
hotkey: 'shift+v',
pageTitle: intl.get('vendors_list'),
defaultSearchResource: RESOURCES_TYPES.VENDOR,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/vendors/contact_duplicate=/:id`,
@@ -449,6 +491,7 @@ export const getDashboardRoutes = () => [
pageTitle: intl.get('new_vendor'),
backLink: true,
defaultSearchResource: RESOURCES_TYPES.VENDOR,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Estimates
@@ -463,6 +506,7 @@ export const getDashboardRoutes = () => [
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.ESTIMATE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/invoices/new?from_estimate_id=/:id`,
@@ -475,6 +519,7 @@ export const getDashboardRoutes = () => [
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.INVOICE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/estimates/new`,
@@ -488,6 +533,7 @@ export const getDashboardRoutes = () => [
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.ESTIMATE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/estimates`,
@@ -499,6 +545,7 @@ export const getDashboardRoutes = () => [
hotkey: 'shift+e',
pageTitle: intl.get('estimates_list'),
defaultSearchResource: RESOURCES_TYPES.ESTIMATE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Invoices.
@@ -513,6 +560,7 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.INVOICE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/invoices/new`,
@@ -526,6 +574,7 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.INVOICE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/invoices`,
@@ -536,6 +585,7 @@ export const getDashboardRoutes = () => [
hotkey: 'shift+i',
pageTitle: intl.get('invoices_list'),
defaultSearchResource: RESOURCES_TYPES.INVOICE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Sales Receipts.
@@ -550,6 +600,7 @@ export const getDashboardRoutes = () => [
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.RECEIPT,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/receipts/new`,
@@ -563,6 +614,7 @@ export const getDashboardRoutes = () => [
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.RECEIPT,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/receipts`,
@@ -573,6 +625,7 @@ export const getDashboardRoutes = () => [
hotkey: 'shift+r',
pageTitle: intl.get('receipts_list'),
defaultSearchResource: RESOURCES_TYPES.RECEIPT,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Payment receives
@@ -589,6 +642,7 @@ export const getDashboardRoutes = () => [
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.PAYMENT_RECEIVE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/payment-receives/new`,
@@ -603,6 +657,7 @@ export const getDashboardRoutes = () => [
backLink: true,
sidebarExpand: false,
defaultSearchResource: RESOURCES_TYPES.PAYMENT_RECEIVE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/payment-receives`,
@@ -614,6 +669,7 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('payment_receives_list'),
pageTitle: intl.get('payment_receives_list'),
defaultSearchResource: RESOURCES_TYPES.PAYMENT_RECEIVE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Bills
@@ -628,6 +684,7 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.BILL,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/bills/new`,
@@ -641,6 +698,7 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.BILL,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/bills`,
@@ -651,6 +709,7 @@ export const getDashboardRoutes = () => [
hotkey: 'shift+b',
pageTitle: intl.get('bills_list'),
defaultSearchResource: RESOURCES_TYPES.BILL,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Subscription billing.
@@ -658,6 +717,7 @@ export const getDashboardRoutes = () => [
path: `/billing`,
component: lazy(() => import('containers/Subscriptions/BillingForm')),
breadcrumb: intl.get('new_billing'),
subscriptionInactive: [SUBSCRIPTION_TYPE.MAIN]
},
// Payment modes.
{
@@ -673,6 +733,7 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.PAYMENT_MADE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/payment-mades/new`,
@@ -687,6 +748,7 @@ export const getDashboardRoutes = () => [
sidebarExpand: false,
backLink: true,
defaultSearchResource: RESOURCES_TYPES.PAYMENT_MADE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
{
path: `/payment-mades`,
@@ -698,11 +760,13 @@ export const getDashboardRoutes = () => [
breadcrumb: intl.get('payment_made_list'),
pageTitle: intl.get('payment_made_list'),
defaultSearchResource: RESOURCES_TYPES.PAYMENT_MADE,
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
// Homepage
{
path: `/`,
component: lazy(() => import('containers/Homepage/Homepage')),
breadcrumb: intl.get('homepage'),
subscriptionActive: [SUBSCRIPTION_TYPE.MAIN],
},
];

View File

@@ -1,69 +1,95 @@
import React from 'react';
import { createReducer } from '@reduxjs/toolkit';
import intl from 'react-intl-universal';
import t from 'store/types';
const getSubscriptionPlans = () => [
{
name: intl.get('Starter'),
slug: 'free',
// slug: 'starter',
description: [
intl.get('Sale and purchase invoices.'),
intl.get('Customers/vendors accounts.'),
intl.get('Expenses tracking'),
intl.get('Manual journals.'),
intl.get('Financial reports.'),
intl.get('For one user and accountant.'),
],
price: {
month: '100',
year: '1,200',
},
currencyCode: 'LYD',
},
{
name: intl.get('Essential'),
slug: 'plus',
description: [
intl.get('All Capital Starter features.'),
intl.get('Multi-currency.'),
intl.get('Purchase and sell orders.'),
intl.get('Inventory management.'),
intl.get('Three users with your accountant.'),
intl.get('Advanced financial reports.'),
],
price: {
month: '200',
year: '2,400',
},
currencyCode: 'LYD',
},
{
name: intl.get('Enterprise'),
slug: 'enterprise',
description: [
intl.get('All Capital Essential features.'),
intl.get('Track multi-branches and locations.'),
intl.get('Projects accounting and timesheets.'),
intl.get('Accounting dimensions.'),
],
price: {
month: '300',
year: '3,400',
},
currencyCode: 'LYD',
},
];
const getSubscriptionPeriods = () => [
{
slug: 'month',
label: intl.get('Monthly'),
label: intl.get('plan.monthly'),
},
{
slug: 'year',
label: intl.get('Yearly'),
label: intl.get('plan.yearly'),
},
];
const getSubscriptionPlans = () => [
{
name: intl.get('plan.essential.title'),
slug: 'free',
description: [
intl.get('plan.feature.sale_purchase_invoice'),
intl.get('plan.feature.receivable_payable_accounts'),
intl.get('plan.feature.expenses_tracking'),
intl.get('plan.feature.manual_journal'),
intl.get('plan.feature.financial_reports'),
intl.get('plan.feature.one_user_with_accountant'),
],
price: '100',
periods: [
{
slug: 'month',
label: intl.get('plan.monthly'),
price: '100'
},
{
slug: 'year',
label: intl.get('plan.yearly'),
price: '1,200',
},
],
currencyCode: 'LYD',
},
{
name: intl.get('plan.professional.title'),
slug: 'plus',
description: [
intl.get('plan.feature.all_capital_essential'),
intl.get('plan.feature.multi_currency'),
intl.get('plan.feature.purchase_sell_orders'),
intl.get('plan.feature.multi_inventory_managment'),
intl.get('plan.feature.three_users'),
intl.get('plan.feature.advanced_financial_reports'),
],
price: '200',
currencyCode: 'LYD',
periods: [
{
slug: 'month',
label: intl.get('plan.monthly'),
price: '200'
},
{
slug: 'year',
label: intl.get('plan.yearly'),
price: '1,200',
},
],
},
{
name: intl.get('plan.plus.title'),
slug: 'enterprise',
description: [
intl.get('plan.feture.all_capital_professional_features'),
intl.get('plan.feature.tracking_multi_locations'),
intl.get('plan.feature.projects_accounting'),
intl.get('plan.feature.accounting_dimensions'),
],
price: '300',
currencyCode: 'LYD',
periods: [
{
slug: 'month',
label: intl.get('plan.monthly'),
price: '300'
},
{
slug: 'year',
label: intl.get('plan.yearly'),
price: '1,200',
},
],
},
];

View File

@@ -4,8 +4,6 @@ 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,
@@ -14,14 +12,6 @@ export const getPlansSelector = () => createSelector(
},
);
// Retrieve manual jounral current page results.
export const getPlansPeriodsSelector = () => createSelector(
plansPeriodsSelector,
(periods) => {
return periods;
},
);
// Retrieve plan details.
export const getPlanSelector = () => createSelector(
planSelector,

View File

@@ -1,23 +1,45 @@
import { createSelector } from '@reduxjs/toolkit';
import { includes } from 'lodash';
const subscriptionSelector = (slug) => (state, props) => {
const subscriptions = Object.values(state.subscriptions.data);
return subscriptions.find((subscription) => subscription.slug === slug);
return subscriptions.find(
(subscription) => subscription.slug === (slug || props.subscriptionType),
);
};
export const isSubscriptionOnTrialFactory = (slug) => createSelector(
const subscriptionsSelector = (state, props) => {
const subscriptions = Object.values(state.subscriptions.data);
return subscriptions.filter(
(subscription) =>
includes(props.subscriptionTypes, subscription.slug) ||
!props.subscriptionTypes,
);
};
export const isSubscriptionOnTrialFactory = (slug) =>
createSelector(
subscriptionSelector(slug),
(subscription) => !!subscription?.on_trial,
);
export const isSubscriptionActiveFactory = (slug) => createSelector(
subscriptionSelector(slug),
(subscription) => {
export const isSubscriptionActiveFactory = (slug) =>
createSelector(subscriptionSelector(slug), (subscription) => {
return !!subscription?.active;
}
);
});
export const isSubscriptionInactiveFactory = (slug) => createSelector(
export const isSubscriptionInactiveFactory = (slug) =>
createSelector(
subscriptionSelector(slug),
(subscription) => !!subscription?.inactive,
);
export const isSubscriptionsInactiveFactory = () =>
createSelector(subscriptionsSelector, (subscriptions) =>
subscriptions.some((subscription) => subscription?.inactive),
);
export const isSubscriptionsActiveFactory = () =>
createSelector(subscriptionsSelector, (subscriptions) =>
subscriptions.some((subscription) => subscription?.active),
);

View File

@@ -69,12 +69,14 @@
&:not([class*="bp3-intent-"]):not(.bp3-minimal) {
color: rgb(255, 255, 255);
}
&:hover,
&:focus,
&:active,
&.bp3-active {
background: transparent;
}
.bp3-button-text {
margin-right: 4px;
text-overflow: ellipsis;
@@ -82,6 +84,7 @@
white-space: nowrap;
display: block;
}
svg {
fill: rgba(255, 255, 255, 0.3);
}
@@ -104,6 +107,10 @@
.sidebar__head-logo {
transition: transform 0.05s ease-in-out;
}
.is-subscription-inactive:not(.sidebar--mini-sidebar) & {
opacity: 0.6;
}
}
&__scroll-wrapper {
@@ -188,6 +195,7 @@
padding-bottom: 6px;
padding-top: 6px;
}
.#{$ns}-menu-item {
padding: 8px 20px;
font-size: 15px;
@@ -242,10 +250,10 @@
opacity: 1;
visibility: visible;
}
// Hide text of bigcapital logo.
&-logo {
}
// Hide text of bigcapital logo.
&-logo {}
&-organization {
opacity: 0;
transition: opacity 0.3s ease-in-out;
@@ -326,8 +334,10 @@
}
}
}
.menu--dashboard-organization {
padding: 10px;
.org-item {
display: flex;
align-items: center;

View File

@@ -2,7 +2,7 @@
.billing-page{
padding: 0 60px;
margin-top: 20px;
margin-top: 40px;
max-width: 820px;
.bp3-tab-list{

View File

@@ -95,6 +95,17 @@ $dashboard-views-bar-height: 44px;
}
}
}
&-subscription-msg{
display: flex;
flex-direction: row;
padding-right: 6px;
font-size: 12px;
span{
margin: auto;
}
}
}
&__breadcrumbs {

View File

@@ -1,5 +1,5 @@
.setup-congrats {
width: 600px;
width: 500px;
margin: 0 auto;
text-align: center;
padding-top: 80px;
@@ -15,7 +15,7 @@
margin-bottom: 12px;
}
.paragraph {
font-size: 15px;
font-size: 16px;
opacity: 0.85;
margin-bottom: 14px;
}

View File

@@ -19,7 +19,9 @@
form {
h3 {
margin-bottom: 1rem;
color: #4e5764;
margin-bottom: 1.2rem;
font-weight: 500;
}
}

View File

@@ -1,3 +1,5 @@
@import "../../Base.scss";
.setup-page {
max-width: 1600px;
@@ -9,15 +11,21 @@
&:before {
content: '';
display: block;
width: 30%;
width: 100%;
height: 1px;
max-width: 350px;
min-width: 350px;
@media only screen and (max-width: 1200px) {
min-width: 300px;
max-width: 350px;
}
}
h1 {
font-size: 22px;
}
h1,
h3 {
font-weight: 500;
@@ -36,11 +44,14 @@
overflow: auto;
z-index: 1;
height: 100%;
width: 30%;
width: 100%;
left: 0;
top: 0;
max-width: 350px;
min-width: 300px;
width: 350px;
@media only screen and (max-width: 1200px) {
width: 300px;
}
.content {
display: flex;
@@ -101,6 +112,7 @@
opacity: 0.75;
padding-bottom: 5px;
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
p>span {
unicode-bidi: plaintext;
}
@@ -133,9 +145,11 @@
margin: 0 auto;
padding: 50px 0 0;
}
ul {
display: flex;
}
li {
position: relative;
list-style-type: none;
@@ -155,6 +169,7 @@
border-radius: 50%;
background-color: #75859c;
}
&::after {
width: 100%;
height: 2px;
@@ -165,21 +180,26 @@
left: -50%;
z-index: -1;
}
&:first-child::after {
display: none;
}
&.is-active {
text-decoration: underline;
&::before {
background-color: #75859c;
}
~li {
&:before,
&:after {
background: #e0e0e0;
}
}
p.wizard-info {
color: #004dd0;
}