mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-16 21:00:31 +00:00
Merge branch 'master' of https://github.com/abouolia/Bigcapital
This commit is contained in:
@@ -64,6 +64,7 @@
|
||||
"postcss-preset-env": "6.7.0",
|
||||
"postcss-safe-parser": "4.0.1",
|
||||
"react": "^16.12.0",
|
||||
"react-albus": "^2.0.0",
|
||||
"react-app-polyfill": "^1.0.6",
|
||||
"react-body-classname": "^1.3.1",
|
||||
"react-dev-utils": "^10.2.0",
|
||||
@@ -83,6 +84,7 @@
|
||||
"react-split-pane": "^0.1.91",
|
||||
"react-table": "^7.0.0",
|
||||
"react-table-sticky": "^1.1.2",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-use": "^13.26.1",
|
||||
"react-window": "^1.8.5",
|
||||
"redux": "^4.0.5",
|
||||
|
||||
8
client/src/common/classes.js
Normal file
8
client/src/common/classes.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
const CLASSES = {
|
||||
DATATABLE_EDITOR: 'DATATABLE_EDITOR'
|
||||
};
|
||||
|
||||
export {
|
||||
CLASSES,
|
||||
}
|
||||
0
client/src/common/errors.js
Normal file
0
client/src/common/errors.js
Normal file
16
client/src/common/registerWizard.js
Normal file
16
client/src/common/registerWizard.js
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
|
||||
export const registerWizardSteps = [
|
||||
{
|
||||
label: 'payment_or_trial',
|
||||
},
|
||||
{
|
||||
label: 'initializing',
|
||||
},
|
||||
{
|
||||
label: 'getting_started',
|
||||
},
|
||||
{
|
||||
label: 'Congratulations',
|
||||
},
|
||||
];
|
||||
41
client/src/common/subscriptionModels.js
Normal file
41
client/src/common/subscriptionModels.js
Normal file
@@ -0,0 +1,41 @@
|
||||
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',
|
||||
},
|
||||
];
|
||||
@@ -5,9 +5,9 @@ import { createBrowserHistory } from 'history';
|
||||
import { ReactQueryConfigProvider } from 'react-query';
|
||||
import { ReactQueryDevtools } from 'react-query-devtools';
|
||||
|
||||
import PrivateRoute from 'components/PrivateRoute';
|
||||
import PrivateRoute from 'components/Guards/PrivateRoute';
|
||||
import Authentication from 'components/Authentication';
|
||||
import Dashboard from 'components/Dashboard/Dashboard';
|
||||
import DashboardPrivatePages from 'components/Dashboard/PrivatePages';
|
||||
import GlobalErrors from 'containers/GlobalErrors/GlobalErrors';
|
||||
|
||||
import messages from 'lang/en';
|
||||
@@ -19,7 +19,7 @@ function App({ locale }) {
|
||||
const queryConfig = {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
}
|
||||
},
|
||||
};
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages}>
|
||||
@@ -32,7 +32,7 @@ function App({ locale }) {
|
||||
</Route>
|
||||
|
||||
<Route path={'/'}>
|
||||
<PrivateRoute component={Dashboard} />
|
||||
<PrivateRoute component={DashboardPrivatePages} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Redirect, Route, Switch, Link } from 'react-router-dom';
|
||||
import { Redirect, Route, Switch, Link, useLocation } from 'react-router-dom';
|
||||
import BodyClassName from 'react-body-classname';
|
||||
import { TransitionGroup, CSSTransition } from 'react-transition-group';
|
||||
import authenticationRoutes from 'routes/authentication';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
import withAuthentication from 'containers/Authentication/withAuthentication';
|
||||
import { compose } from 'utils';
|
||||
import Icon from 'components/Icon';
|
||||
|
||||
function PageFade(props) {
|
||||
return (
|
||||
<CSSTransition {...props} classNames="authTransition" timeout={500} />
|
||||
);
|
||||
}
|
||||
|
||||
function AuthenticationWrapper({ isAuthorized = false, ...rest }) {
|
||||
const to = { pathname: '/homepage' };
|
||||
const location = useLocation();
|
||||
const locationKey = location.pathname;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -15,27 +25,37 @@ function AuthenticationWrapper({ isAuthorized = false, ...rest }) {
|
||||
<Redirect to={to} />
|
||||
) : (
|
||||
<BodyClassName className={'authentication'}>
|
||||
<Switch>
|
||||
<div class="authentication-page">
|
||||
<Link
|
||||
to={'bigcapital.io'}
|
||||
className={'authentication-page__goto-bigcapital'}
|
||||
>
|
||||
<T id={'go_to_bigcapital_com'} />
|
||||
</Link>
|
||||
<div class="authentication-page">
|
||||
<Link
|
||||
to={'bigcapital.io'}
|
||||
className={'authentication-page__goto-bigcapital'}
|
||||
>
|
||||
<T id={'go_to_bigcapital_com'} />
|
||||
</Link>
|
||||
|
||||
<div class="authentication-page__form-wrapper">
|
||||
{authenticationRoutes.map((route, index) => (
|
||||
<Route
|
||||
key={index}
|
||||
path={route.path}
|
||||
exact={route.exact}
|
||||
component={route.component}
|
||||
/>
|
||||
))}
|
||||
<div class="authentication-page__form-wrapper">
|
||||
<div class="authentication-insider">
|
||||
<div className={'authentication-insider__logo-section'}>
|
||||
<Icon icon="bigcapital" height={37} width={214} />
|
||||
</div>
|
||||
|
||||
<TransitionGroup>
|
||||
<PageFade key={locationKey}>
|
||||
<Switch>
|
||||
{authenticationRoutes.map((route, index) => (
|
||||
<Route
|
||||
key={index}
|
||||
path={route.path}
|
||||
exact={route.exact}
|
||||
component={route.component}
|
||||
/>
|
||||
))}
|
||||
</Switch>
|
||||
</PageFade>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</Switch>
|
||||
</div>
|
||||
</BodyClassName>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
import classNames from 'classnames';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import DashboardLoadingIndicator from './DashboardLoadingIndicator';
|
||||
|
||||
import Sidebar from 'components/Sidebar/Sidebar';
|
||||
import DashboardContent from 'components/Dashboard/DashboardContent';
|
||||
@@ -10,28 +12,47 @@ import PreferencesSidebar from 'components/Preferences/PreferencesSidebar';
|
||||
import Search from 'containers/GeneralSearch/Search';
|
||||
import DashboardSplitPane from 'components/Dashboard/DashboardSplitePane';
|
||||
|
||||
export default function Dashboard() {
|
||||
|
||||
import withSettingsActions from 'containers/Settings/withSettingsActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
function Dashboard({
|
||||
// #withSettings
|
||||
requestFetchOptions,
|
||||
}) {
|
||||
const fetchOptions = useQuery(
|
||||
['options'], () => requestFetchOptions(),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classNames('dashboard')}>
|
||||
<Switch>
|
||||
<Route path="/preferences">
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<PreferencesSidebar />
|
||||
</DashboardSplitPane>
|
||||
<PreferencesContent />
|
||||
</Route>
|
||||
|
||||
<DashboardLoadingIndicator isLoading={fetchOptions.isFetching}>
|
||||
<Switch>
|
||||
<Route path="/preferences">
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<PreferencesSidebar />
|
||||
</DashboardSplitPane>
|
||||
<PreferencesContent />
|
||||
</Route>
|
||||
|
||||
<Route path="/">
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<DashboardContent />
|
||||
</DashboardSplitPane>
|
||||
</Route>
|
||||
</Switch>
|
||||
<Route path="/">
|
||||
<DashboardSplitPane>
|
||||
<Sidebar />
|
||||
<DashboardContent />
|
||||
</DashboardSplitPane>
|
||||
</Route>
|
||||
</Switch>
|
||||
|
||||
<Search />
|
||||
<DialogsContainer />
|
||||
</div>
|
||||
<Search />
|
||||
<DialogsContainer />
|
||||
</DashboardLoadingIndicator>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withSettingsActions,
|
||||
)(Dashboard);
|
||||
25
client/src/components/Dashboard/DashboardLoadingIndicator.js
Normal file
25
client/src/components/Dashboard/DashboardLoadingIndicator.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { Choose, Icon } from 'components';
|
||||
|
||||
export default function DashboardLoadingIndicator({
|
||||
isLoading = false,
|
||||
className,
|
||||
children,
|
||||
}) {
|
||||
return (
|
||||
<Choose>
|
||||
<Choose.When condition={isLoading}>
|
||||
<div className={classNames('bigcapital-loading', className)}>
|
||||
<div class="center">
|
||||
<Icon icon="bigcapital" height={37} width={214} />
|
||||
</div>
|
||||
</div>
|
||||
</Choose.When>
|
||||
|
||||
<Choose.Otherwise>
|
||||
{ children }
|
||||
</Choose.Otherwise>
|
||||
</Choose>
|
||||
);
|
||||
}
|
||||
63
client/src/components/Dashboard/PrivatePages.js
Normal file
63
client/src/components/Dashboard/PrivatePages.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { Switch, Route } from 'react-router';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import Dashboard from 'components/Dashboard/Dashboard';
|
||||
import SetupWizardPage from 'containers/Setup/WizardSetupPage';
|
||||
import DashboardLoadingIndicator from 'components/Dashboard/DashboardLoadingIndicator';
|
||||
|
||||
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
|
||||
import withSubscriptionsActions from 'containers/Subscriptions/withSubscriptionsActions';
|
||||
|
||||
import EnsureOrganizationIsReady from 'components/Guards/EnsureOrganizationIsReady';
|
||||
import EnsureOrganizationIsNotReady from 'components/Guards/EnsureOrganizationIsNotReady';
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Dashboard inner private pages.
|
||||
*/
|
||||
function DashboardPrivatePages({
|
||||
|
||||
// #withOrganizationActions
|
||||
requestAllOrganizations,
|
||||
|
||||
// #withSubscriptionsActions
|
||||
requestFetchSubscriptions,
|
||||
}) {
|
||||
// Fetch all user's organizatins.
|
||||
const fetchOrganizations = useQuery(
|
||||
['organizations'], () => requestAllOrganizations(),
|
||||
);
|
||||
|
||||
// Fetchs organization subscriptions.
|
||||
const fetchSuscriptions = useQuery(
|
||||
['susbcriptions'], () => requestFetchSubscriptions(),
|
||||
{ enabled: fetchOrganizations.data },
|
||||
)
|
||||
|
||||
return (
|
||||
<DashboardLoadingIndicator isLoading={
|
||||
fetchOrganizations.isFetching ||
|
||||
fetchSuscriptions.isFetching
|
||||
}>
|
||||
<Switch>
|
||||
<Route path={'/setup'}>
|
||||
<EnsureOrganizationIsNotReady>
|
||||
<SetupWizardPage />
|
||||
</EnsureOrganizationIsNotReady>
|
||||
</Route>
|
||||
|
||||
<Route path='/'>
|
||||
<EnsureOrganizationIsReady>
|
||||
<Dashboard />
|
||||
</EnsureOrganizationIsReady>
|
||||
</Route>
|
||||
</Switch>
|
||||
</DashboardLoadingIndicator>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withOrganizationActions,
|
||||
withSubscriptionsActions,
|
||||
)(DashboardPrivatePages);
|
||||
34
client/src/components/Guards/EnsureOrganizationIsNotReady.js
Normal file
34
client/src/components/Guards/EnsureOrganizationIsNotReady.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { compose } from 'utils';
|
||||
import withAuthentication from 'containers/Authentication/withAuthentication';
|
||||
import withOrganization from 'containers/Organization/withOrganization';
|
||||
|
||||
function EnsureOrganizationIsNotReady({
|
||||
children,
|
||||
|
||||
// #withOrganization
|
||||
isOrganizationReady,
|
||||
isOrganizationSetupCompleted
|
||||
}) {
|
||||
return (isOrganizationReady && !isOrganizationSetupCompleted) ? (
|
||||
<Redirect to={{ pathname: '/' }} />
|
||||
) : children;
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAuthentication(({ currentOrganizationId }) => ({
|
||||
currentOrganizationId,
|
||||
})),
|
||||
connect((state, props) => ({
|
||||
organizationId: props.currentOrganizationId,
|
||||
})),
|
||||
withOrganization(({
|
||||
isOrganizationReady,
|
||||
isOrganizationSetupCompleted
|
||||
}) => ({
|
||||
isOrganizationReady,
|
||||
isOrganizationSetupCompleted
|
||||
})),
|
||||
)(EnsureOrganizationIsNotReady);
|
||||
31
client/src/components/Guards/EnsureOrganizationIsReady.js
Normal file
31
client/src/components/Guards/EnsureOrganizationIsReady.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { compose } from 'utils';
|
||||
|
||||
import withAuthentication from 'containers/Authentication/withAuthentication';
|
||||
import withOrganization from 'containers/Organization/withOrganization';
|
||||
|
||||
|
||||
function EnsureOrganizationIsReady({
|
||||
// #ownProps
|
||||
children,
|
||||
redirectTo = '/setup',
|
||||
|
||||
// #withOrganizationByOrgId
|
||||
isOrganizationInitialized,
|
||||
}) {
|
||||
return (isOrganizationInitialized) ? children : (
|
||||
<Redirect
|
||||
to={{ pathname: redirectTo }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAuthentication(),
|
||||
connect((state, props) => ({
|
||||
organizationId: props.currentOrganizationId,
|
||||
})),
|
||||
withOrganization(({ isOrganizationInitialized }) => ({ isOrganizationInitialized })),
|
||||
)(EnsureOrganizationIsReady);
|
||||
@@ -7,17 +7,10 @@ export default function AuthInsider({
|
||||
copyright = true,
|
||||
children,
|
||||
}) {
|
||||
|
||||
return (
|
||||
<div class="authentication-insider">
|
||||
<div className={'authentication-insider__logo-section'}>
|
||||
<Icon
|
||||
icon='bigcapital'
|
||||
height={37}
|
||||
width={214} />
|
||||
</div>
|
||||
<div class="authentication-insider__content">
|
||||
|
||||
<div class="authentication-insider__content">
|
||||
<div class="authentication-insider__form">
|
||||
{ children }
|
||||
</div>
|
||||
|
||||
@@ -25,5 +18,5 @@ export default function AuthInsider({
|
||||
<AuthCopyright />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import withAuthenticationActions from './withAuthenticationActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
const ERRORS_TYPES = {
|
||||
INVALID_DETAILS: 'INVALID_DETAILS',
|
||||
USER_INACTIVE: 'USER_INACTIVE',
|
||||
|
||||
@@ -13,16 +13,16 @@ import {
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
import Icon from 'components/Icon';
|
||||
import { If } from 'components';
|
||||
import AuthInsider from 'containers/Authentication/AuthInsider';
|
||||
import withAuthenticationActions from './withAuthenticationActions';
|
||||
import withAuthenticationActions from 'containers/Authentication/withAuthenticationActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function Register({ requestRegister }) {
|
||||
function RegisterUserForm({ requestRegister, requestLogin }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const history = useHistory();
|
||||
const [shown, setShown] = useState(false);
|
||||
@@ -31,9 +31,6 @@ function Register({ requestRegister }) {
|
||||
}, [shown]);
|
||||
|
||||
const ValidationSchema = Yup.object().shape({
|
||||
organization_name: Yup.string()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'organization_name_' })),
|
||||
first_name: Yup.string()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'first_name_' })),
|
||||
@@ -56,7 +53,6 @@ function Register({ requestRegister }) {
|
||||
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
organization_name: '',
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
@@ -82,14 +78,20 @@ function Register({ requestRegister }) {
|
||||
onSubmit: (values, { setSubmitting, setErrors }) => {
|
||||
requestRegister(values)
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'welcome_organization_account_has_been_created',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
history.push('/auth/login');
|
||||
requestLogin({
|
||||
crediential: values.email,
|
||||
password: values.password,
|
||||
})
|
||||
.then(() => {
|
||||
history.push('/register/subscription');
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch((errors) => {
|
||||
AppToaster.show({
|
||||
message: formatMessage({ id: 'something_wentwrong' }),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((errors) => {
|
||||
if (errors.some((e) => e.type === 'PHONE_NUMBER_EXISTS')) {
|
||||
@@ -150,31 +152,7 @@ function Register({ requestRegister }) {
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className={'authentication-page__form'}>
|
||||
<FormGroup
|
||||
label={<T id={'organization_name'} />}
|
||||
className={'form-group--name'}
|
||||
intent={
|
||||
errors.organization_name &&
|
||||
touched.organization_name &&
|
||||
Intent.DANGER
|
||||
}
|
||||
helperText={
|
||||
<ErrorMessage
|
||||
{...{ errors, touched }}
|
||||
name={'organization_name'}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<InputGroup
|
||||
intent={
|
||||
errors.organization_name &&
|
||||
touched.organization_name &&
|
||||
Intent.DANGER
|
||||
}
|
||||
{...getFieldProps('organization_name')}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
<Row className={'name-section'}>
|
||||
<Col md={6}>
|
||||
<FormGroup
|
||||
@@ -301,4 +279,6 @@ function Register({ requestRegister }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(withAuthenticationActions)(Register);
|
||||
export default compose(
|
||||
withAuthenticationActions,
|
||||
)(RegisterUserForm);
|
||||
|
||||
@@ -19,21 +19,20 @@ import withAuthenticationActions from './withAuthenticationActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
|
||||
|
||||
function ResetPassword({ requestResetPassword }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const { token } = useParams();
|
||||
const history = useHistory();
|
||||
|
||||
|
||||
const ValidationSchema = Yup.object().shape({
|
||||
password: Yup.string()
|
||||
.min(4)
|
||||
.required().label(formatMessage({id:'password'})),
|
||||
.required()
|
||||
.label(formatMessage({ id: 'password' })),
|
||||
confirm_password: Yup.string()
|
||||
.oneOf([Yup.ref('password'), null])
|
||||
.required().label(formatMessage({id:'confirm_password'})),
|
||||
.required()
|
||||
.label(formatMessage({ id: 'confirm_password' })),
|
||||
});
|
||||
|
||||
const initialValues = useMemo(
|
||||
@@ -41,7 +40,7 @@ function ResetPassword({ requestResetPassword }) {
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
}),
|
||||
[]
|
||||
[],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -89,14 +88,14 @@ function ResetPassword({ requestResetPassword }) {
|
||||
<T id={'choose_a_new_password'} />
|
||||
</h3>
|
||||
<T id={'you_remembered_your_password'} />{' '}
|
||||
<Link to='/auth/login'>
|
||||
<Link to="/auth/login">
|
||||
<T id={'login'} />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<FormGroup
|
||||
label={<T id={'password'} />}
|
||||
label={<T id={'new_password'} />}
|
||||
intent={errors.password && touched.password && Intent.DANGER}
|
||||
helperText={
|
||||
<ErrorMessage name={'password'} {...{ errors, touched }} />
|
||||
@@ -144,10 +143,10 @@ function ResetPassword({ requestResetPassword }) {
|
||||
fill={true}
|
||||
className={'btn-new'}
|
||||
intent={Intent.PRIMARY}
|
||||
type='submit'
|
||||
type="submit"
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'submit_new_password'} />
|
||||
<T id={'submit'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -69,13 +69,13 @@ function SendResetPassword({ requestSendResetPassword }) {
|
||||
|
||||
return (
|
||||
<AuthInsider>
|
||||
<div class='reset-form'>
|
||||
<div className='reset-form'>
|
||||
<div className={'authentication-page__label-section'}>
|
||||
<h3>
|
||||
<T id={'reset_your_password'} />
|
||||
<T id={'you_can_t_login'} />
|
||||
</h3>
|
||||
<p>
|
||||
<T id={'we_ll_send_you_a_link_to_reset_your_password'} />
|
||||
<T id={'we_ll_send_a_recovery_link_to_your_email'} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -104,7 +104,7 @@ function SendResetPassword({ requestSendResetPassword }) {
|
||||
fill={true}
|
||||
loading={isSubmitting}
|
||||
>
|
||||
<T id={'send_password_reset_link'} />
|
||||
<T id={'send_reset_password_mail'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -6,6 +6,7 @@ export default (mapState) => {
|
||||
const mapped = {
|
||||
isAuthorized: isAuthenticated(state),
|
||||
user: state.authentication.user,
|
||||
currentOrganizationId: state.authentication?.organizationId,
|
||||
};
|
||||
return mapState ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
|
||||
40
client/src/containers/Organization/withOrganization.js
Normal file
40
client/src/containers/Organization/withOrganization.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
getOrganizationByIdFactory,
|
||||
isOrganizationReadyFactory,
|
||||
isOrganizationSeededFactory,
|
||||
isOrganizationBuiltFactory,
|
||||
isOrganizationSeedingFactory,
|
||||
isOrganizationInitializingFactory,
|
||||
isOrganizationSubscribedFactory,
|
||||
isOrganizationCongratsFactory,
|
||||
} from 'store/organizations/organizations.selectors';
|
||||
|
||||
export default (mapState) => {
|
||||
const getOrganizationById = getOrganizationByIdFactory();
|
||||
const isOrganizationReady = isOrganizationReadyFactory();
|
||||
|
||||
const isOrganizationSeeded = isOrganizationSeededFactory();
|
||||
const isOrganizationBuilt = isOrganizationBuiltFactory();
|
||||
|
||||
const isOrganizationInitializing = isOrganizationInitializingFactory();
|
||||
const isOrganizationSeeding = isOrganizationSeedingFactory();
|
||||
|
||||
const isOrganizationSubscribed = isOrganizationSubscribedFactory();
|
||||
const isOrganizationCongrats = isOrganizationCongratsFactory();
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
organization: getOrganizationById(state, props),
|
||||
isOrganizationReady: isOrganizationReady(state, props),
|
||||
isOrganizationSeeded: isOrganizationSeeded(state, props),
|
||||
isOrganizationInitialized: isOrganizationBuilt(state, props),
|
||||
isOrganizationSeeding: isOrganizationInitializing(state, props),
|
||||
isOrganizationInitializing: isOrganizationSeeding(state, props),
|
||||
isOrganizationSubscribed: isOrganizationSubscribed(state, props),
|
||||
isOrganizationSetupCompleted: isOrganizationCongrats(state, props),
|
||||
};
|
||||
return (mapState) ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
return connect(mapStateToProps);
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fetchOrganizations,
|
||||
buildTenant,
|
||||
seedTenant,
|
||||
setOrganizationSetupCompleted,
|
||||
} from 'store/organizations/organizations.actions';
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
requestOrganizationBuild: () => dispatch(buildTenant()),
|
||||
requestOrganizationSeed: () => dispatch(seedTenant()),
|
||||
requestAllOrganizations: () => dispatch(fetchOrganizations()),
|
||||
setOrganizationSetupCompleted: (congrats) => dispatch(setOrganizationSetupCompleted(congrats)),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
@@ -40,7 +40,6 @@ function GeneralPreferences({
|
||||
}) {
|
||||
const { formatMessage } = useIntl();
|
||||
const [selectedItems, setSelectedItems] = useState({});
|
||||
const [timeZone, setTimeZone] = useState('');
|
||||
|
||||
const fetchHook = useQuery(
|
||||
['settings'],
|
||||
@@ -65,67 +64,89 @@ function GeneralPreferences({
|
||||
{ id: 1, name: 'Libyan Dinar ', value: 'LYD' },
|
||||
];
|
||||
|
||||
// @todo @mohamed - Translate the months.
|
||||
// eg. > `${formatMessage({ id: 'january' })} - ${formatMessage({ id: 'december' })}`
|
||||
const fiscalYear = [
|
||||
{
|
||||
id: 0,
|
||||
name: `${formatMessage({ id: 'january' })} - ${formatMessage({ id: 'december' })}`,
|
||||
name: `${formatMessage({ id: 'january' })} - ${formatMessage({
|
||||
id: 'december',
|
||||
})}`,
|
||||
value: 'january',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: `${formatMessage({ id: 'february' })} - ${formatMessage({ id: 'january' })}`,
|
||||
name: `${formatMessage({ id: 'february' })} - ${formatMessage({
|
||||
id: 'january',
|
||||
})}`,
|
||||
value: 'february',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: `${formatMessage({ id: 'march' })} - ${formatMessage({ id: 'february' })}`,
|
||||
name: `${formatMessage({ id: 'march' })} - ${formatMessage({
|
||||
id: 'february',
|
||||
})}`,
|
||||
value: 'March',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: `${formatMessage({ id: 'april' })} - ${formatMessage({ id: 'march' })}`,
|
||||
name: `${formatMessage({ id: 'april' })} - ${formatMessage({
|
||||
id: 'march',
|
||||
})}`,
|
||||
value: 'april',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: `${formatMessage({ id: 'may' })} - ${formatMessage({ id: 'april' })}`,
|
||||
name: `${formatMessage({ id: 'may' })} - ${formatMessage({
|
||||
id: 'april',
|
||||
})}`,
|
||||
value: 'may',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: `${formatMessage({ id: 'june' })} - ${formatMessage({ id: 'may' })}`,
|
||||
name: `${formatMessage({ id: 'june' })} - ${formatMessage({
|
||||
id: 'may',
|
||||
})}`,
|
||||
value: 'june',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: `${formatMessage({ id: 'july' })} - ${formatMessage({ id: 'june' })}`,
|
||||
name: `${formatMessage({ id: 'july' })} - ${formatMessage({
|
||||
id: 'june',
|
||||
})}`,
|
||||
value: 'july',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: `${formatMessage({ id: 'august' })} - ${formatMessage({ id: 'july' })}`,
|
||||
name: `${formatMessage({ id: 'august' })} - ${formatMessage({
|
||||
id: 'july',
|
||||
})}`,
|
||||
value: 'August',
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: `${formatMessage({ id: 'september' })} - ${formatMessage({ id: 'august' })}`,
|
||||
name: `${formatMessage({ id: 'september' })} - ${formatMessage({
|
||||
id: 'august',
|
||||
})}`,
|
||||
value: 'september',
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
name: `${formatMessage({ id: 'october' })} - ${formatMessage({ id: 'november' })}`,
|
||||
name: `${formatMessage({ id: 'october' })} - ${formatMessage({
|
||||
id: 'november',
|
||||
})}`,
|
||||
value: 'october',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: `${formatMessage({ id: 'november' })} - ${formatMessage({ id: 'october' })}`,
|
||||
name: `${formatMessage({ id: 'november' })} - ${formatMessage({
|
||||
id: 'october',
|
||||
})}`,
|
||||
value: 'november',
|
||||
},
|
||||
{
|
||||
id: 11,
|
||||
name: `${formatMessage({ id: 'december' })} - ${formatMessage({ id: 'november' })}`,
|
||||
name: `${formatMessage({ id: 'december' })} - ${formatMessage({
|
||||
id: 'november',
|
||||
})}`,
|
||||
value: 'december',
|
||||
},
|
||||
];
|
||||
@@ -185,9 +206,9 @@ function GeneralPreferences({
|
||||
language: Yup.string()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'language' })),
|
||||
// time_zone: Yup.string()
|
||||
// .required()
|
||||
// .label(formatMessage({ id: 'time_zone' })),
|
||||
time_zone: Yup.string()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'time_zone_' })),
|
||||
date_format: Yup.string()
|
||||
.required()
|
||||
.label(formatMessage({ id: 'date_format_' })),
|
||||
@@ -230,15 +251,8 @@ function GeneralPreferences({
|
||||
},
|
||||
});
|
||||
|
||||
// @todo @mohamed remove duplicate functions.
|
||||
|
||||
|
||||
const onItemRenderer = (item, { handleClick }) => (
|
||||
<MenuItem
|
||||
key={item.id}
|
||||
text={item.name}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
<MenuItem key={item.id} text={item.name} onClick={handleClick} />
|
||||
);
|
||||
|
||||
const currencyItem = (item, { handleClick }) => (
|
||||
@@ -280,8 +294,12 @@ function GeneralPreferences({
|
||||
}
|
||||
};
|
||||
|
||||
const handleTimezoneChange = (timezone) => setTimeZone(timezone);
|
||||
|
||||
const handleTimezoneChange = useCallback(
|
||||
(timezone) => {
|
||||
setFieldValue('time_zone', timezone);
|
||||
},
|
||||
[setFieldValue],
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
@@ -437,7 +455,7 @@ function GeneralPreferences({
|
||||
}
|
||||
>
|
||||
<TimezonePicker
|
||||
value={timeZone}
|
||||
value={values.time_zone}
|
||||
onChange={handleTimezoneChange}
|
||||
valueDisplayFormat="composite"
|
||||
placeholder={<T id={'select_time_zone'} />}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { omit } from 'lodash';
|
||||
import { Button, Intent, Position, Tooltip } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
|
||||
import CLASSES from 'components/classes';
|
||||
import { CLASSES } from 'common/classes';
|
||||
import DataTable from 'components/DataTable';
|
||||
import Icon from 'components/Icon';
|
||||
import {
|
||||
|
||||
50
client/src/containers/Setup/SetupCongratsPage.js
Normal file
50
client/src/containers/Setup/SetupCongratsPage.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
import { useHistory } from "react-router-dom";
|
||||
import WorkflowIcon from './WorkflowIcon';
|
||||
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
function SetupCongratsPage({
|
||||
setOrganizationSetupCompleted,
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
const handleBtnClick = useCallback(() => {
|
||||
setOrganizationSetupCompleted(false);
|
||||
history.push('/');
|
||||
}, [
|
||||
setOrganizationSetupCompleted,
|
||||
history,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div class="setup-congrats">
|
||||
<div class="setup-congrats__workflow-pic">
|
||||
<WorkflowIcon width="280" height="330" />
|
||||
</div>
|
||||
|
||||
<div class="setup-congrats__text">
|
||||
<h1>Congrats! You are ready to go</h1>
|
||||
|
||||
<p class="paragraph">
|
||||
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
type="submit"
|
||||
onClick={handleBtnClick}
|
||||
>
|
||||
Go to dashboard
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withOrganizationActions,
|
||||
)(SetupCongratsPage);
|
||||
51
client/src/containers/Setup/SetupInitializingForm.js
Normal file
51
client/src/containers/Setup/SetupInitializingForm.js
Normal file
@@ -0,0 +1,51 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
import { withWizard } from 'react-albus'
|
||||
import { ProgressBar, Intent } from '@blueprintjs/core';
|
||||
|
||||
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
|
||||
import withOrganization from 'containers/Organization/withOrganization'
|
||||
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Setup initializing step form.
|
||||
*/
|
||||
function SetupInitializingForm({
|
||||
|
||||
// #withOrganizationActions
|
||||
requestOrganizationBuild,
|
||||
|
||||
wizard: { next },
|
||||
}) {
|
||||
const { isSuccess } = useQuery(
|
||||
['build-tenant'], () => requestOrganizationBuild(),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isSuccess) {
|
||||
next();
|
||||
}
|
||||
}, [isSuccess, next]);
|
||||
|
||||
return (
|
||||
<div class="setup-initializing-form">
|
||||
<ProgressBar intent={Intent.PRIMARY} value={null} />
|
||||
<div className={'setup-initializing-form__title'}>
|
||||
<h1>
|
||||
{/* You organization is initializin... */}
|
||||
It's time to make your accounting really simple!
|
||||
</h1>
|
||||
<p className={'paragraph'}>
|
||||
while we set up your account, please remember to verify your account by
|
||||
clicking on the link we sent to yout registered email address
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withOrganizationActions,
|
||||
withWizard,
|
||||
)(SetupInitializingForm);
|
||||
75
client/src/containers/Setup/SetupLeftSection.js
Normal file
75
client/src/containers/Setup/SetupLeftSection.js
Normal file
@@ -0,0 +1,75 @@
|
||||
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';
|
||||
|
||||
|
||||
function FooterLinkItem({ title, link }) {
|
||||
return (
|
||||
<div class="">
|
||||
<a href={link} target="_blank">{ title }</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wizard setup left section.
|
||||
*/
|
||||
function SetupLeftSection({
|
||||
// #withAuthenticationActions
|
||||
requestLogout,
|
||||
|
||||
// #withAuthentication
|
||||
currentOrganizationId
|
||||
}) {
|
||||
const onClickLogout = useCallback(() => {
|
||||
requestLogout();
|
||||
}, [requestLogout]);
|
||||
|
||||
return (
|
||||
<section className={'setup-page__left-section'}>
|
||||
<div className={'content'}>
|
||||
<div className={'content__logo'}>
|
||||
<Icon icon="bigcapital" className={'bigcapital--alt'} height={37} 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">
|
||||
Oragnization ID: <span class="id">{ currentOrganizationId }</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'} /> {'+21892-791-8381'}</p>
|
||||
</div>
|
||||
|
||||
<div className={'content__links'}>
|
||||
<For render={FooterLinkItem} of={footerLinks} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAuthenticationActions,
|
||||
withAuthentication(({ currentOrganizationId }) => ({ currentOrganizationId })),
|
||||
)(SetupLeftSection);
|
||||
436
client/src/containers/Setup/SetupOrganizationForm.js
Normal file
436
client/src/containers/Setup/SetupOrganizationForm.js
Normal file
@@ -0,0 +1,436 @@
|
||||
import React, { useMemo, useState, useCallback } from 'react';
|
||||
import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { Row, Col } from 'react-grid-system';
|
||||
import {
|
||||
Button,
|
||||
Intent,
|
||||
FormGroup,
|
||||
InputGroup,
|
||||
MenuItem,
|
||||
Classes,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
import moment from 'moment';
|
||||
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 { momentFormatter, tansformDateValue } from 'utils';
|
||||
import { ListSelect, ErrorMessage, FieldRequiredHint } from 'components';
|
||||
|
||||
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],
|
||||
);
|
||||
|
||||
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 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'} />}
|
||||
labelProp={'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'} />}
|
||||
labelProp={'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'} />}
|
||||
labelProp={'name'}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{/* Time zone */}
|
||||
<FormGroup
|
||||
label={<T id={'time_zone'} />}
|
||||
labelInfo={<FieldRequiredHint />}
|
||||
className={classNames(
|
||||
'form-group--time-zone',
|
||||
'form-group--select-list',
|
||||
Classes.FILL,
|
||||
)}
|
||||
intent={errors.time_zone && touched.time_zone && Intent.DANGER}
|
||||
helperText={
|
||||
<ErrorMessage {...{ errors, touched }} name={'time_zone'} />
|
||||
}
|
||||
>
|
||||
<TimezonePicker
|
||||
value={values.time_zone}
|
||||
onChange={handleTimeZoneChange}
|
||||
valueDisplayFormat="composite"
|
||||
showLocalTimezone={true}
|
||||
placeholder={<T id={'select_time_zone'} />}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<p className={'register-org-note'}>
|
||||
<T
|
||||
id={
|
||||
'note_you_can_change_your_preferences_later_in_dashboard_if_needed'
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
<div className={'register-org-button'}>
|
||||
<Button
|
||||
intent={Intent.PRIMARY}
|
||||
disabled={isSubmitting}
|
||||
loading={isSubmitting}
|
||||
type="submit"
|
||||
>
|
||||
<T id={'save_continue'} />
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withSettingsActions,
|
||||
withOrganizationActions,
|
||||
withWizard,
|
||||
)(SetupOrganizationForm);
|
||||
115
client/src/containers/Setup/SetupRightSection.js
Normal file
115
client/src/containers/Setup/SetupRightSection.js
Normal file
@@ -0,0 +1,115 @@
|
||||
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 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 withAuthentication from 'containers/Authentication/withAuthentication';
|
||||
import withOrganization from 'containers/Organization/withOrganization'
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* Wizard setup right section.
|
||||
*/
|
||||
function SetupRightSection ({
|
||||
// #withAuthentication
|
||||
currentOrganizationId,
|
||||
|
||||
// #withOrganization
|
||||
isOrganizationInitialized,
|
||||
isOrganizationSeeded,
|
||||
isOrganizationSetupCompleted,
|
||||
|
||||
// #withSubscriptions
|
||||
isSubscriptionActive
|
||||
}) {
|
||||
const history = useHistory();
|
||||
|
||||
const handleSkip = useCallback(({ step, push }) => {
|
||||
const scenarios = [
|
||||
{ condition: isOrganizationSetupCompleted, redirectTo: 'congrats' },
|
||||
{ condition: !isSubscriptionActive, redirectTo: 'subscription' },
|
||||
{ condition: isSubscriptionActive && !isOrganizationInitialized, redirectTo: 'initializing' },
|
||||
{ condition: isSubscriptionActive && !isOrganizationSeeded, 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'}>
|
||||
<Wizard
|
||||
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>
|
||||
)} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withAuthentication(({ currentOrganizationId }) => ({ currentOrganizationId })),
|
||||
connect((state, props) => ({
|
||||
organizationId: props.currentOrganizationId,
|
||||
})),
|
||||
withOrganization(({
|
||||
organization,
|
||||
isOrganizationInitialized,
|
||||
isOrganizationSeeded,
|
||||
isOrganizationSetupCompleted
|
||||
}) => ({
|
||||
organization,
|
||||
isOrganizationInitialized,
|
||||
isOrganizationSeeded,
|
||||
isOrganizationSetupCompleted
|
||||
})),
|
||||
withSubscriptions(({
|
||||
isSubscriptionActive,
|
||||
}) => ({
|
||||
isSubscriptionActive
|
||||
}), 'main'),
|
||||
)(SetupRightSection);
|
||||
93
client/src/containers/Setup/SetupSubscriptionForm.js
Normal file
93
client/src/containers/Setup/SetupSubscriptionForm.js
Normal file
@@ -0,0 +1,93 @@
|
||||
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 withSubscriptionsActions from 'containers/Subscriptions/withSubscriptionsActions';
|
||||
import BillingPlans from 'containers/Subscriptions/billingPlans';
|
||||
import BillingPeriods from 'containers/Subscriptions/billingPeriods';
|
||||
import { BillingPaymentmethod } from 'containers/Subscriptions/billingPaymentmethod';
|
||||
import withBillingActions from 'containers/Subscriptions/withBillingActions';
|
||||
import { compose } from 'utils';
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
},
|
||||
});
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
export default compose(
|
||||
withBillingActions,
|
||||
withWizard,
|
||||
withSubscriptionsActions,
|
||||
)(SetupSubscriptionForm);
|
||||
13
client/src/containers/Setup/WizardSetupPage.js
Normal file
13
client/src/containers/Setup/WizardSetupPage.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
import SetupRightSection from './SetupRightSection';
|
||||
import SetupLeftSection from './SetupLeftSection';
|
||||
|
||||
|
||||
export default function WizardSetupPage() {
|
||||
return (
|
||||
<div class="setup-page">
|
||||
<SetupLeftSection />
|
||||
<SetupRightSection />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
36
client/src/containers/Setup/WizardSetupSteps.js
Normal file
36
client/src/containers/Setup/WizardSetupSteps.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage as T } from 'react-intl';
|
||||
import { registerWizardSteps } from 'common/registerWizard'
|
||||
|
||||
function WizardSetupStep({
|
||||
label,
|
||||
isActive = false
|
||||
}) {
|
||||
return (
|
||||
<li className={classNames({ 'is-active': isActive })}>
|
||||
<p className={'wizard-info'}><T id={label} /></p>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function WizardSetupSteps({
|
||||
currentStep = 1,
|
||||
}) {
|
||||
return (
|
||||
<div className={'setup-page-steps-container'}>
|
||||
<div className={'setup-page-steps'}>
|
||||
<ul>
|
||||
{registerWizardSteps.map((step, index) => (
|
||||
<WizardSetupStep
|
||||
label={step.label}
|
||||
isActive={(index + 1) == currentStep}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WizardSetupSteps;
|
||||
120
client/src/containers/Setup/WorkflowIcon.js
Normal file
120
client/src/containers/Setup/WorkflowIcon.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function WorkflowIcon({
|
||||
width = '309.566',
|
||||
height = '356.982',
|
||||
}) {
|
||||
return (
|
||||
<svg width={width} height={height} viewBox="0 0 309.566 356.982">
|
||||
<defs>
|
||||
<linearGradient id="linear-gradient" x1="-0.001" y1="0.5" x2="1" y2="0.5" gradientUnits="objectBoundingBox">
|
||||
<stop offset="0.561"/>
|
||||
<stop offset="1" stop-color="#1244ee"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="_2_workflow" data-name="2_workflow" transform="translate(-0.237)">
|
||||
<g id="background_1_" transform="translate(0 6)">
|
||||
<g id="background">
|
||||
<rect id="Rectangle" width="8.559" height="21.34" transform="translate(178.613 224.585) rotate(90)" fill="#b600a0"/>
|
||||
<path id="Path" d="M13.7,42.04a.992.992,0,0,1-.827-.559c-.73-1.264-1.728-6.3-3.042-15.342C4.31,24.849,1.22,23.877.417,23.147a1.347,1.347,0,0,1-.389-1.24c.146-.778.292-1.678,9.587-4.085C12.315.486,13.362.195,13.994.024A.605.605,0,0,1,14.189,0a.992.992,0,0,1,.827.559c.779,1.337,1.874,7.173,3.309,17.288,9.246,2.407,9.392,3.282,9.538,4.085a1.434,1.434,0,0,1-.389,1.24c-.8.729-3.918,1.726-9.514,3.015C15.479,41.627,14.554,41.87,13.9,42.064A.605.605,0,0,0,13.7,42.04Z" transform="translate(23.43 136.648)" fill="#ffe700"/>
|
||||
<path id="Path-2" data-name="Path" d="M10.741,26.6a.8.8,0,0,1-.657-.365c-.56-.8-1.338-3.963-2.385-9.7-4.331-.8-6.74-1.435-7.373-1.9a.823.823,0,0,1-.316-.778c.1-.486.219-1.07,7.519-2.577C9.646.316,10.474.146,10.96.024c.049,0,.1-.024.17-.024a.8.8,0,0,1,.657.365c.608.851,1.484,4.523,2.6,10.917,7.251,1.508,7.373,2.091,7.47,2.577a.733.733,0,0,1-.316.778c-.633.462-3.066,1.094-7.446,1.9-1.947,9.75-2.677,9.92-3.188,10.018A.619.619,0,0,1,10.741,26.6Z" transform="translate(227.55 181.46)" fill="#b600a0"/>
|
||||
<path id="Path-3" data-name="Path" d="M6.925,17.166a.478.478,0,0,1-.414-.219c-.365-.511-.876-2.577-1.533-6.273A19.951,19.951,0,0,1,.209,9.458a.475.475,0,0,1-.195-.511c.073-.316.146-.681,4.867-1.678C6.244.195,6.779.073,7.1,0h.1a.478.478,0,0,1,.414.219C8,.778,8.556,3.137,9.286,7.27c4.672.973,4.769,1.337,4.818,1.653a.475.475,0,0,1-.195.511,19.373,19.373,0,0,1-4.818,1.24c-1.265,6.3-1.728,6.395-2.068,6.468C7,17.142,6.95,17.166,6.925,17.166Z" transform="translate(157.783 23.366)" fill="#00dbc5"/>
|
||||
<rect id="Rectangle-2" data-name="Rectangle" width="8.57" height="21.335" transform="translate(34.445 239.399) rotate(-45)" fill="#00dbc5"/>
|
||||
<rect id="Rectangle-3" data-name="Rectangle" width="7.206" height="17.904" transform="translate(288.717 63.421) rotate(137.939)" fill="#ffe700"/>
|
||||
<rect id="Rectangle-4" data-name="Rectangle" width="7.205" height="17.908" transform="translate(282.646 316.784) rotate(57.16)" fill="#ffe700"/>
|
||||
<rect id="Rectangle-5" data-name="Rectangle" width="7.207" height="17.901" transform="translate(9.188 309.907) rotate(30.001)" fill="#ffe700"/>
|
||||
<rect id="Rectangle-6" data-name="Rectangle" width="8.57" height="21.335" transform="translate(28.993 81.118) rotate(135)" fill="#b600a0"/>
|
||||
<rect id="Rectangle-7" data-name="Rectangle" width="8.568" height="21.338" transform="translate(74.022 307.725) rotate(57.16)" fill="#b600a0"/>
|
||||
<rect id="Rectangle-8" data-name="Rectangle" width="8.568" height="21.338" transform="translate(305.157 237.249) rotate(57.16)" fill="#b600a0"/>
|
||||
<rect id="Rectangle-9" data-name="Rectangle" width="8.569" height="21.337" transform="matrix(-0.635, -0.772, 0.772, -0.635, 284.433, 175)" fill="#00dbc5"/>
|
||||
<rect id="Rectangle-10" data-name="Rectangle" width="6.719" height="16.689" transform="translate(88.407 98.397) rotate(47.939)" fill="#f73200"/>
|
||||
<rect id="Rectangle-11" data-name="Rectangle" width="6.716" height="16.692" transform="matrix(0.37, 0.929, -0.929, 0.37, 154.831, 324.521)" fill="#f73200"/>
|
||||
<rect id="Rectangle-12" data-name="Rectangle" width="6.719" height="16.689" transform="translate(238.03 0.186) rotate(47.939)" fill="#f73200"/>
|
||||
<rect id="Rectangle-13" data-name="Rectangle" width="6.719" height="16.69" transform="translate(234.897 259.449) rotate(-53.216)" fill="#f73200"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="main_1_" transform="translate(40)">
|
||||
<g id="main">
|
||||
<g id="laptop">
|
||||
<path id="Path-4" data-name="Path" d="M85.3,61.617l-34.49-6.936L52.2,15.4,1.846,0,.291,44.509h-.1L0,51.98,85.157,67.75Z" transform="translate(0.024)" fill="#5078ff"/>
|
||||
<path id="Path-5" data-name="Path" d="M44.23,13.19q-.583,15.477-1.19,30.955l-8.453-1.679c-2.137-.438-4.3-.852-6.437-1.29-1.919-.389-3.838-.754-5.732-1.144-4.712-.925-9.424-1.874-14.112-2.8-2.769-.56-5.538-1.1-8.307-1.655Q.474,17.8.923,0c5.1,1.557,10.2,3.091,15.278,4.648,4.275,1.29,8.525,2.6,12.8,3.894,2.016.608,4.008,1.217,6.024,1.825,1.409.438,2.818.852,4.251,1.29C40.9,12.168,42.578,12.679,44.23,13.19Z" transform="translate(5.125 6.644)" fill="#121212"/>
|
||||
<path id="Path-6" data-name="Path" d="M19.48,11.876c-2.915,6.79-7.335,8.371-8.161,13.19-.389,2.215-.073,5.6,3.5,10.294C10.1,34.435,5.392,33.485.7,32.561A24.983,24.983,0,0,1,0,27.207a24.559,24.559,0,0,1,.389-4.332C1.652,16.207,7.311,13.409,8.72,6.644A16.014,16.014,0,0,0,8.623,0c4.275,1.29,8.525,2.6,12.8,3.894A25.525,25.525,0,0,1,19.48,11.876Z" transform="translate(12.703 11.34)" fill="#fff"/>
|
||||
<path id="Path-7" data-name="Path" d="M5.364,20.442C4.489,23.459,5.194,27.256,8.5,32.1c-2.137-.438-4.3-.852-6.437-1.29-2.793-6.5-2.332-10.586-1.044-13.263,1.846-3.8,5.319-4.7,7.165-9.369A15.4,15.4,0,0,0,8.91,0c1.409.438,2.818.852,4.251,1.29a18.7,18.7,0,0,1-2.016,10.878C9.007,16.037,6.359,16.986,5.364,20.442Z" transform="translate(31.215 17.01)" fill="#fff"/>
|
||||
</g>
|
||||
<g id="lines" transform="translate(138.932 70.086)">
|
||||
<path id="Path-8" data-name="Path" d="M1.025,24.335C-1.5,19.785.953,14.845,4.984,12.216,9.964,8.98,16.109,9.588,21.622,8.055a11.184,11.184,0,0,0,6.437-4.04A9.3,9.3,0,0,0,29.565,0h2.016V3.48a12.85,12.85,0,0,1-7.724,6.911c-5.562,2.02-12.12.973-17.269,4.234-1.967,1.241-3.765,3.285-3.74,5.743.024,1.9,1.239,4.259,3.3,4.526,1.846.243,1.846,3.164,0,2.92A6.643,6.643,0,0,1,1.025,24.335Z" transform="translate(36.841 0.073)" fill="#f73200"/>
|
||||
<path id="Path-9" data-name="Path" d="M20.16,31.9c-5.635-.876-10.881-4.5-14.768-8.517A26.168,26.168,0,0,0,0,18.544V15.331c3.3,1.387,5.878,4.526,8.477,7.033a27.352,27.352,0,0,0,7.432,5.11c2.623,1.241,6.072,2.458,8.89,1.144,2.21-1.022,3.376-3.31,2.21-5.573-1.142-2.166-3.23-3.7-4.761-5.6-3.328-4.137-3.886-9.758-2.307-14.723A17.826,17.826,0,0,1,21.058,0h3.328a15.03,15.03,0,0,0-1.822,12c.753,2.434,2.356,4.137,4.056,5.938,1.554,1.63,3.23,3.456,3.668,5.743A6.625,6.625,0,0,1,27.859,30.1,9.466,9.466,0,0,1,20.16,31.9Z" transform="translate(0.146 0.073)" fill="#f73200"/>
|
||||
<path id="Path-10" data-name="Path" d="M35.826,13.039h-3.4A25.208,25.208,0,0,0,28.734,7.9c-3.813-3.845-9.3-6.011-14.67-4.526C9.133,4.74,6.607,9.194,3.716,13.039H0c.049-.049.1-.122.146-.17C3.109,9.413,5.2,5.2,9.011,2.55,13.432-.54,19.431-.686,24.289,1.382s8.428,6.035,10.93,10.586C35.389,12.26,35.583,12.649,35.826,13.039Z" transform="translate(19.965 37.116)" fill="#f73200"/>
|
||||
<path id="Path-11" data-name="Path" d="M8.671,0V3.675C7.287,4.843,6,6.133,4.639,7.349H0c.461-.316.874-.657,1.239-.925C3.425,4.745,5.295,2.726,7.432.973,7.845.633,8.258.316,8.671,0Z" transform="translate(59.775 42.806)" fill="#f73200"/>
|
||||
</g>
|
||||
<g id="pencil" transform="translate(180.466 96.368)">
|
||||
<path id="Path-12" data-name="Path" d="M41.145,7.4A1.72,1.72,0,0,1,39.154,8.42l-3.206-.511-.316-.049L31.26,7.155l-1.044-.17-2.72-.438L23.73,5.938h-.073l-4.542-.73-.121-.024h0c-.7-1.046-1.36-1.776-2.259-1.8A2.348,2.348,0,0,0,14.768,4.5c-2.842-.462-5.684-.925-8.5-1.363-1.02-.438-2.04-.852-3.06-1.29C2.137,1.387,1.069.949,0,.487,1.117.414,2.259.316,3.376.243,4.518.17,5.635.073,6.777,0l6.631,1.071c.753,1.29,3.352,1.265,5.756.925l1.554.243,5.708.925,3.668.584.1.024.146.024,3.935.633.389.049h0l.923.146,4.178.681A1.7,1.7,0,0,1,41.145,7.4Z" transform="translate(0.243 0.122)"/>
|
||||
<path id="Path-13" data-name="Path" d="M3.425.487a2.874,2.874,0,0,1-.219,1.119C2.137,1.144,1.069.706,0,.243,1.117.17,2.259.073,3.376,0A1.579,1.579,0,0,1,3.425.487Z" transform="translate(0.194 0.341)" fill="#fff"/>
|
||||
</g>
|
||||
<g id="man" transform="translate(36.19 44.534)">
|
||||
<path id="legs" d="M137.518,141.462c-5.951,6.546-10.177,9.515-12.776,11-.51.292-5.562,3.115-5.659,2.969v-.024C118.33,153.63,81.459,43.658,80.026,39.254a150.168,150.168,0,0,1-25.6-2.75A149.09,149.09,0,0,1,28.68,29.033,45.251,45.251,0,0,1,39.95,39.619C48.111,50.545,47.771,60.888,51,72.885a79.775,79.775,0,0,0,9.91,22.267,95.832,95.832,0,0,0,6.024,8.493h0c.413.535.826,1.046,1.263,1.582h0c-.583.195-1.166.414-1.749.608-1.069.365-2.137.754-3.206,1.119-4.2,1.484-8.4,2.945-12.582,4.429h0l-4.809,1.679v.024c-1.142-1.533-2.332-3.188-3.595-4.94-3.692-5.208-7.845-11.438-12.144-18.544C15.394,65.1-9.089,24.287,3.468,7.52,10.609-2.019,27.344-.608,54.4,1.68c7.87.657,14.525,1.728,17.488,2.239a206.377,206.377,0,0,1,35.632,9.466s6.679,17.546,9.06,29.227a152.42,152.42,0,0,1,3.085,29.933A40.938,40.938,0,0,1,128.9,84.59c6.121,12.314,2.623,21.853,5.732,42.733A142.242,142.242,0,0,0,137.518,141.462Z" transform="translate(0.175 118.367)" fill="#ff6334"/>
|
||||
<g id="head_1_" transform="translate(84.525 36.503)">
|
||||
<path id="head" d="M26.6,22.924c-.243,2.628-.559,6.23-.874,9.637A61.076,61.076,0,0,0,12,28.862l.559-3.821c-3.5-.608-6.461-2.142-7.8-4.4-2.162-3.675.073-9.126,2.55-12.63L0,.876,10.979,4.064A18.6,18.6,0,0,1,18.994,0a7.076,7.076,0,0,0,1.943,3.358,6.74,6.74,0,0,0,5.2,1.728,13.946,13.946,0,0,0-3.716,9.685C22.516,17.424,23.682,22.121,26.6,22.924Z" transform="translate(0.121 2.166)" fill="#ff8f6d"/>
|
||||
<path id="hair" d="M13.77,5.751C9.932.373,1.6-1.087.241.762A1.731,1.731,0,0,0,.047,2.1,7.076,7.076,0,0,0,1.99,5.459a6.74,6.74,0,0,0,5.2,1.728,13.946,13.946,0,0,0-3.716,9.685c.146,2.653,1.287,7.374,4.2,8.152.024,0,.049,0,.049.024a4.822,4.822,0,0,0,3.473-.754C15.738,21.4,18.069,11.786,13.77,5.751Z" transform="translate(19.044 0.065)" fill="url(#linear-gradient)"/>
|
||||
<path id="mouth" d="M4.639,3.577A7.563,7.563,0,0,1,0,1.922L1.117.657.559,1.29,1.117.657C1.263.779,4.736,3.723,9.035,0l1.117,1.29A8.275,8.275,0,0,1,4.639,3.577Z" transform="translate(4.931 16.67)"/>
|
||||
<ellipse id="Oval" cx="1.142" cy="1.144" rx="1.142" ry="1.144" transform="translate(12.217 6.79)"/>
|
||||
<path id="brow" d="M0,0H1.7l0,5.3H0Z" transform="matrix(0.967, -0.255, 0.255, 0.967, 15.94, 2.629)"/>
|
||||
<path id="glasses" d="M8.72,6.06A4.529,4.529,0,1,1,8.986,4.5a4.719,4.719,0,0,1-.049.584l8.356,4.5-.437.852Z" transform="translate(9.303 4.113)"/>
|
||||
<path id="ear" d="M.729,2.119C4.906-.1,8.647-.631,10.1.781a1.917,1.917,0,0,1,.607,1.29c.219,2.482-4.493,5.281-8.793,5.548A10.925,10.925,0,0,1,0,7.57C.267,5.721.51,3.92.729,2.119Z" transform="translate(20.84 12.287)" fill="#ff8f6d"/>
|
||||
</g>
|
||||
<g id="sneakers" transform="translate(36.19 221.939)">
|
||||
<g id="Group">
|
||||
<path id="Path-14" data-name="Path" d="M42.147,6.765,41.079,7.933l-1.53,1.655-1.943,2.069-1.53,1.655L36,13.384l-.486.535-1.6,1.728-1.02,1.1-.534.535L30.464,19.2l-.656.681L5.01,44.826l-.437.438-.85.852H3.7c-.1-.049-.194-.122-.291-.17-.073-.049-.121-.073-.194-.122s-.17-.122-.243-.17-.17-.122-.243-.17a1.572,1.572,0,0,1-.219-.195,1.389,1.389,0,0,1-.194-.17l-.024-.024c-.073-.073-.146-.122-.219-.195a1.573,1.573,0,0,1-.194-.219c-.049-.073-.121-.122-.17-.195-.049-.049-.1-.122-.146-.17a.436.436,0,0,1-.121-.17,3.481,3.481,0,0,1-.267-.365,6.716,6.716,0,0,1-.68-1.217,5.991,5.991,0,0,1-.486-2.044A5.391,5.391,0,0,1,.152,39.01l.874-2.92L1.658,34l1.093-3.577a2.947,2.947,0,0,0,.121-.438L9.576,7.86V7.836l4.809-1.679q6.3-2.19,12.582-4.429C28.035,1.363,29.1.973,30.173.608,30.756.414,31.339.195,31.922,0h0c.024,0,.049.024.073.049s.073.049.121.1c.121.073.267.17.461.292.024.024.073.049.1.073L32.7.535h0c.024.024.024.024.049.024A.052.052,0,0,1,32.8.608c.049.024.073.049.121.073.024,0,.024.024.049.024.049.049.121.073.17.122s.121.073.17.122h0c.024,0,.049.024.073.049.049.024.073.049.121.073.049.049.121.073.17.122a.024.024,0,0,1,.024.024,1.348,1.348,0,0,1,.194.146l.437.292,1.457.973c.559.365,1.142.754,1.725,1.144.049.049.121.073.17.122.121.1.267.17.389.268.024.024.049.024.073.049l.656.438,1.312.876c.267.17.534.365.8.535.121.1.267.17.389.268a.335.335,0,0,0,.1.073c.121.1.267.17.389.268A.542.542,0,0,1,42.147,6.765Z" transform="translate(0.237 1.655)" fill="#4d00ff"/>
|
||||
<path id="Path-15" data-name="Path" d="M42.141,6.79,41.072,7.958l-1.53,1.655L37.6,11.681l-1.53,1.655L36,13.409l-.486.535-1.6,1.728-1.02,1.1-.534.535-1.895,1.922-.656.681L5,44.85l-.437.438-.85.852H3.692c-.1-.049-.194-.122-.291-.17-.073-.049-.121-.073-.194-.122s-.17-.122-.243-.17-.17-.122-.243-.17a1.572,1.572,0,0,1-.219-.195,1.389,1.389,0,0,1-.194-.17l-.024-.024c-.073-.073-.146-.122-.219-.195A1.573,1.573,0,0,1,1.87,44.7c-.049-.073-.121-.122-.17-.195-.049-.049-.1-.122-.146-.17a.436.436,0,0,1-.121-.17,3.482,3.482,0,0,1-.267-.365,6.716,6.716,0,0,1-.68-1.217A5.991,5.991,0,0,1,0,40.543a18.335,18.335,0,0,0,2.065.292c5.684.341,9.351-5.84,22.71-19.42,1.19-1.192,2.137-2.166,2.72-2.726a28.992,28.992,0,0,1-1.8-8.128,30.53,30.53,0,0,1,.389-7.52V3.018c.729-.341,1.457-.681,2.186-1.046.583-.268,1.142-.56,1.725-.827h0C30.628.754,31.284.389,31.916,0h0c.024.024.049.024.073.049s.073.049.121.1c.121.073.267.17.461.292.024.024.073.049.1.073l.024.024h0a.531.531,0,0,0,.121.073h0c.049.024.073.049.121.073.024,0,.024.024.049.024.049.049.121.073.17.122s.121.073.17.122h0c.024.024.049.024.073.049.049.024.073.049.121.073.024.024.073.049.1.073s.073.049.1.073a1.348,1.348,0,0,1,.194.146l.437.292,1.457.973c.559.365,1.142.754,1.725,1.144.049.049.121.073.17.122.121.1.267.17.389.268.024.024.049.024.073.049l.656.438,1.312.876c.267.17.534.365.8.535.121.1.267.17.389.268a.335.335,0,0,0,.1.073c.121.1.267.17.389.268A.708.708,0,0,1,42.141,6.79Z" transform="translate(0.243 1.63)" fill="#fff"/>
|
||||
<path id="Path-16" data-name="Path" d="M13.782,6.79,12.714,7.958l-1.53,1.655L9.24,11.681,7.71,13.336l-.073.073-.486.535-1.6,1.728-1.02,1.1-.534.535L2.1,19.225l-.656.681C-.524,14.8-.524,8.031,1.711,1.168V1.144h0c.049-.17.121-.341.17-.535C2.464.414,3.047.195,3.63,0h0c.024,0,.049.024.073.049s.073.049.121.1c.121.073.267.17.461.292.024.024.073.049.1.073l.024.024h0c.024.024.024.024.049.024A.052.052,0,0,1,4.5.608h0c.049.024.073.049.121.073.024,0,.024.024.049.024.049.049.121.073.17.122s.121.073.17.122h0c.024,0,.049.024.073.049.049.024.073.049.121.073.024.024.073.049.1.073s.049.024.073.049a.024.024,0,0,1,.024.024,1.347,1.347,0,0,1,.194.146l.437.292,1.457.973c.559.365,1.142.754,1.725,1.144.049.049.121.073.17.122.121.1.267.17.389.268.024.024.049.024.073.049l.656.438,1.312.876c.267.17.534.365.8.535.121.1.267.17.389.268a.335.335,0,0,0,.1.073c.121.1.267.17.389.268A.747.747,0,0,1,13.782,6.79Z" transform="translate(28.602 1.63)" fill="#b600a0"/>
|
||||
<path id="Path-17" data-name="Path" d="M46.246,6.107c-.243.243-.51.487-.777.73-.559.535-1.093,1.071-1.652,1.606-.68.681-1.384,1.338-2.065,2-.559.535-1.093,1.071-1.652,1.606l-2.332,2.263c-.559.535-1.093,1.071-1.652,1.606-2.842,2.726-5.659,5.475-8.5,8.2C23.9,27.693,19.868,31,16.395,34.823h0c-.146.17-.291.341-.461.511,0,0-5.854,6.619-9.424,7.325a5.241,5.241,0,0,1-3.255-.292.266.266,0,0,1-.1-.049,5.157,5.157,0,0,1-.85-.511,6.974,6.974,0,0,1-1-.9,4.845,4.845,0,0,1-.7-.925A5.953,5.953,0,0,1,0,38.79a13.121,13.121,0,0,0,3.206-.073c6.291-.925,9.3-6.181,18.144-15.112,1.749-1.752,4.421-4.38,7.918-7.447l1.53-3.139.947-1.971c.461-.925.9-1.849,1.36-2.774L34.053,6.3c.413-.827.8-1.655,1.19-2.482h0l.947-1.971c.291-.608.583-1.217.9-1.825h0c.559-.049,5.028-.292,7.651,2.969A7.289,7.289,0,0,1,46.246,6.107Z" transform="translate(0.729 5.403)" fill="#4aba6b"/>
|
||||
<path id="Path-18" data-name="Path" d="M10.25,5.013C9.691,5.548,9.157,6.084,8.6,6.619l-4.809-2.6L0,1.971H0L.947,0,5.319,2.361Z" transform="translate(35.972 7.228)" fill="#ffe323"/>
|
||||
<path id="Path-19" data-name="Path" d="M8.7,4.161C8.137,4.7,7.6,5.232,7.044,5.767L2.477,3.31,0,1.971.947,0,3.983,1.655Z" transform="translate(33.81 11.681)" fill="#ffe323"/>
|
||||
<path id="Path-20" data-name="Path" d="M7.019,3.261C6.461,3.8,5.926,4.332,5.368,4.867L1.044,2.531,0,1.971.947,0,2.6.9Z" transform="translate(31.503 16.451)" fill="#ffe323"/>
|
||||
<ellipse id="Oval-2" data-name="Oval" cx="3.279" cy="3.285" rx="3.279" ry="3.285" transform="translate(16.614 14.991)" fill="#f73200"/>
|
||||
<path id="Path-21" data-name="Path" d="M15.108,0c-.146.17-.291.341-.461.511,0,0-5.854,6.619-9.424,7.325A5.146,5.146,0,0,1,1.846,7.5,5.157,5.157,0,0,1,1,6.984a6.974,6.974,0,0,1-1-.9,5.117,5.117,0,0,1,2.162.292c.9.365,1,.73,1.652.852,1.943.341,3.5-2.482,5.756-4.453A13.466,13.466,0,0,1,15.108,0Z" transform="translate(2.04 40.226)" fill="#f73200"/>
|
||||
<path id="Path-22" data-name="Path" d="M31.114,1.752c-.729.341-1.433.681-2.137,1.022-1.312.633-2.6,1.241-3.911,1.849-.121.073-.267.122-.389.195-3.158,1.509-6.388,3.018-9.594,4.526a124.082,124.082,0,0,0-4.566,13.7c-.34,1.265-.632,2.458-.923,3.6-1.457,5.743-2.5,9.9-5.926,11.073a5.259,5.259,0,0,1-1.749.268A6.942,6.942,0,0,1,0,37.7L.632,35.6a4.008,4.008,0,0,0,2.332.024c2.307-.779,3.23-4.429,4.493-9.515.291-1.168.583-2.361.923-3.626A131.076,131.076,0,0,1,13.189,8.128l.146-.365h0V7.739l.389-.195c3.352-1.557,6.7-3.164,9.983-4.721C25.673,1.9,27.641.949,29.608,0h0c.413.535.826,1.046,1.263,1.582.024,0,.049.024.073.049A.561.561,0,0,0,31.114,1.752Z" transform="translate(1.287 0.024)" fill="#ffe323"/>
|
||||
</g>
|
||||
<g id="Group-2" data-name="Group" transform="translate(80.639 37.72)">
|
||||
<path id="Path-23" data-name="Path" d="M33.737,4.307l-.8,1.363L31.818,7.641,30.41,10.1,29.268,12.07l-.049.1-.364.608-1.19,2.044-.753,1.314-.413.657-1.433,2.288-.486.8L6.072,49.742l-.316.511-.632,1.022H5.1c-.1-.024-.219-.073-.316-.1L4.566,51.1a1.484,1.484,0,0,1-.291-.122c-.1-.049-.17-.073-.267-.122s-.17-.073-.267-.122a1.134,1.134,0,0,0-.243-.122.024.024,0,0,1-.024-.024c-.1-.049-.17-.1-.267-.146a1.474,1.474,0,0,1-.243-.17l-.219-.146c-.073-.049-.121-.1-.17-.122a1.028,1.028,0,0,0-.17-.122,4.231,4.231,0,0,1-.34-.292,6.466,6.466,0,0,1-.947-1.046,5.867,5.867,0,0,1-.923-1.874A6.112,6.112,0,0,1,0,45.166c.073-1.022.146-2.02.194-3.042l.146-2.19c.073-1.241.17-2.482.243-3.723,0-.146.024-.292.024-.438q.8-11.535,1.554-23.07v-.024c1.433-.9,2.866-1.825,4.3-2.726q5.647-3.577,11.27-7.13C18.678,2.215,19.65,1.606,20.6,1c.534-.341,1.044-.657,1.579-1h0c.024,0,.049.024.073.024.049.024.1.024.146.049.121.049.291.122.51.195.024.024.073.024.121.049.024,0,.024,0,.049.024h0a.085.085,0,0,1,.049.024c.024,0,.049.024.073.024.049.024.073.024.121.049.024,0,.024,0,.049.024a1.672,1.672,0,0,0,.194.073c.073.024.121.049.194.073h0c.024,0,.049.024.073.024.049.024.1.024.146.049a1.672,1.672,0,0,0,.194.073h.024a1.673,1.673,0,0,1,.243.1,4.045,4.045,0,0,1,.486.195c.51.195,1.044.389,1.652.608.607.243,1.263.487,1.943.73.073.024.121.049.194.073.146.049.291.122.437.17.024,0,.049.024.073.024.243.1.486.195.729.268.486.195.972.365,1.457.56.291.122.607.219.9.341.146.049.291.122.437.17.024.024.073.024.1.049.146.049.291.122.437.17A2.345,2.345,0,0,1,33.737,4.307Z" transform="translate(0.17 1.436)" fill="#4d00ff"/>
|
||||
<path id="Path-24" data-name="Path" d="M33.543,4.283l-.8,1.363L31.624,7.617l-1.409,2.458-1.142,1.971-.049.1-.364.608L27.471,14.8l-.753,1.314-.413.657-1.433,2.288-.486.8L5.878,49.717l-.316.511L4.931,51.25H4.906c-.1-.024-.219-.073-.316-.1l-.219-.073a1.484,1.484,0,0,1-.291-.122c-.1-.049-.17-.073-.267-.122s-.17-.073-.267-.122a1.134,1.134,0,0,0-.243-.122.024.024,0,0,1-.024-.024c-.1-.049-.17-.1-.267-.146a1.474,1.474,0,0,1-.243-.17l-.219-.146c-.073-.049-.121-.1-.17-.122a1.028,1.028,0,0,0-.17-.122,4.231,4.231,0,0,1-.34-.292,6.466,6.466,0,0,1-.947-1.046A5.867,5.867,0,0,1,0,46.651c.656-.049,1.384-.049,2.089-.17,5.611-.949,7.8-7.787,17.779-24.019.874-1.436,1.6-2.58,2.04-3.261A29.04,29.04,0,0,1,17.026,4.283V4.259c.632-.487,1.263-1,1.895-1.509.51-.389,1-.8,1.482-1.192h0L22.03,0h0c.024,0,.049.024.073.024.049.024.1.024.146.049.121.049.291.122.51.195.024.024.073.024.121.049.024,0,.024,0,.049.024h0c.049.024.073.024.121.049h0c.049.024.073.024.121.049.024,0,.024,0,.049.024a1.672,1.672,0,0,0,.194.073c.073.024.121.049.194.073h0c.024,0,.049.024.073.024.049.024.1.024.146.049.024.024.073.024.1.049.049.024.073.024.121.049a1.673,1.673,0,0,1,.243.1,4.045,4.045,0,0,1,.486.195c.51.195,1.044.389,1.652.608.607.243,1.263.487,1.943.73.073.024.121.049.194.073.146.049.291.122.437.17.024,0,.049.024.073.024.243.1.486.195.729.268.486.195.972.365,1.457.56.291.122.607.219.9.341.146.049.291.122.437.17.024.024.073.024.1.049.146.049.291.122.437.17A.951.951,0,0,1,33.543,4.283Z" transform="translate(0.364 1.46)" fill="#fff"/>
|
||||
<path id="Path-25" data-name="Path" d="M13.256,4.332l-.8,1.363L11.338,7.666,9.929,10.124,8.787,12.095l-.049.1-.364.608-1.19,2.044-.753,1.314-.413.657L4.585,19.1l-.486.8C1.039,15.356-.491,8.785.14,1.582V1.557h0C.165,1.363.165,1.192.189,1,.723.657,1.233.341,1.768,0h0c.024,0,.049.024.073.024.049.024.1.024.146.049.121.049.291.122.51.195.024.024.073.024.121.049.024,0,.024,0,.049.024h0a.085.085,0,0,1,.049.024c.024,0,.049.024.073.024h0c.049.024.073.024.121.049.024,0,.024,0,.049.024a1.672,1.672,0,0,0,.194.073c.073.024.121.049.194.073h0c.024,0,.049.024.073.024.049.024.1.024.146.049.024.024.073.024.1.049.024,0,.049.024.073.024h.024A1.673,1.673,0,0,1,4,.852a4.045,4.045,0,0,1,.486.195c.51.195,1.044.389,1.652.608.607.243,1.263.487,1.943.73.073.024.121.049.194.073.146.049.291.122.437.17.024,0,.049.024.073.024.243.1.486.195.729.268.486.195.972.365,1.457.56.291.122.607.219.9.341.146.049.291.122.437.17.024.024.073.024.1.049.146.049.291.122.437.17C13.038,4.259,13.159,4.307,13.256,4.332Z" transform="translate(20.651 1.411)" fill="#b600a0"/>
|
||||
<path id="Path-26" data-name="Path" d="M37.672,4.362c-.194.292-.389.608-.583.9-.413.657-.826,1.29-1.239,1.922-.51.8-1.044,1.606-1.554,2.434-.413.633-.826,1.29-1.239,1.922-.583.9-1.166,1.8-1.749,2.726-.413.633-.826,1.29-1.239,1.922-2.137,3.31-4.3,6.619-6.437,9.9-2.818,4.332-6,8.444-8.525,12.971h0c-.121.195-.219.389-.34.584,0,0-4.226,7.763-7.554,9.247a5.39,5.39,0,0,1-3.255.462.424.424,0,0,0-.121-.024,5.5,5.5,0,0,1-.923-.292,4.732,4.732,0,0,1-1.166-.657,7.41,7.41,0,0,1-.9-.73A5.683,5.683,0,0,1,0,46.633a13.164,13.164,0,0,0,3.085-.8c5.926-2.336,7.7-8.128,14.282-18.811,1.312-2.117,3.328-5.281,6.048-9.053.267-1.144.51-2.263.777-3.407l.486-2.142c.243-1,.461-2.02.68-3.018.17-.706.316-1.436.486-2.142.194-.9.413-1.776.607-2.677h0c.17-.706.34-1.411.486-2.142.146-.657.291-1.314.461-2h0c.534-.17,4.833-1.411,8.137,1.192A7.862,7.862,0,0,1,37.672,4.362Z" transform="translate(1.409 3.352)" fill="#4aba6b"/>
|
||||
<path id="Path-27" data-name="Path" d="M10.614,2.8c-.413.657-.826,1.29-1.239,1.922L4.129,3.285,0,2.142H0C.17,1.436.34.73.486,0L5.271,1.314Z" transform="translate(27.884 5.792)" fill="#ffe323"/>
|
||||
<path id="Path-28" data-name="Path" d="M8.914,2.361c-.413.633-.826,1.29-1.239,1.922L2.7,2.9,0,2.142C.17,1.436.316.706.486,0L3.838.925Z" transform="translate(26.766 10.586)" fill="#ffe323"/>
|
||||
<path id="Path-29" data-name="Path" d="M7.117,1.849c-.413.633-.826,1.29-1.239,1.922L1.142,2.458,0,2.142.486,0l1.8.487Z" transform="translate(25.552 15.745)" fill="#ffe323"/>
|
||||
<ellipse id="Oval-3" data-name="Oval" cx="3.279" cy="3.285" rx="3.279" ry="3.285" transform="translate(10.93 17.108)" fill="#f73200"/>
|
||||
<path id="Path-30" data-name="Path" d="M13.335,0c-.121.195-.219.389-.34.584,0,0-4.226,7.763-7.554,9.247a5.16,5.16,0,0,1-3.352.414,5.5,5.5,0,0,1-.923-.292A4.732,4.732,0,0,1,0,9.3,4.677,4.677,0,0,1,2.186,9.1c.972.146,1.142.487,1.8.462,1.967-.1,2.842-3.212,4.615-5.621A13.012,13.012,0,0,1,13.335,0Z" transform="translate(3.109 42.417)" fill="#f73200"/>
|
||||
<path id="Path-31" data-name="Path" d="M22.249,1.363c-.632.487-1.239,1-1.87,1.484-1.142.9-2.259,1.8-3.4,2.7-.121.1-.219.17-.34.268C13.893,7.982,11.1,10.2,8.307,12.387A121.987,121.987,0,0,0,6.922,26.769c-.049,1.314-.073,2.531-.1,3.7C6.7,36.406,6.631,40.689,3.546,42.587a4.979,4.979,0,0,1-1.627.657A6.158,6.158,0,0,1,0,43.39L.146,41.2A3.717,3.717,0,0,0,2.4,40.689c2.065-1.265,2.137-5.062,2.259-10.294.024-1.192.049-2.434.1-3.748A128.5,128.5,0,0,1,6.242,11.584l.073-.389h0V11.17l.34-.268c2.915-2.288,5.829-4.575,8.671-6.838,1.7-1.363,3.4-2.7,5.125-4.064h0c.51.414,1.044.827,1.6,1.265.024,0,.049.024.073.024A.134.134,0,0,0,22.249,1.363Z" transform="translate(0.437 0.146)" fill="#ffe323"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="body" transform="translate(31.818 25.552)">
|
||||
<path id="Path-32" data-name="Path" d="M151.581,52.053c-4.178-1.509-8.331-2.993-12.509-4.5-.146-.049-.316-.122-.461-.17-2.21,4.4-4.785,9.077-7.772,13.944-.1.146-.194.316-.291.462-.972,1.557-1.943,3.091-2.939,4.551-3.036,4.575-6.1,8.688-9.035,12.362A105.832,105.832,0,0,0,105,63.442a102.07,102.07,0,0,0-23.172-16.28c-1.142-.56-2.283-1.046-3.425-1.533a61.076,61.076,0,0,0-13.723-3.7,81.255,81.255,0,0,0-14.816-.852c-2.477.073-4.858.243-7.165.487A113.616,113.616,0,0,0,20.3,46.286c-.267-6.23-.34-12.849-.146-19.858.049-1.679.1-3.31.17-4.94.1-2.482.243-4.891.413-7.276.219-3.212.486-6.3.8-9.32l-.51-.146C15.807,3.237,10.585,1.7,5.338.195,5.12.122,4.9.073,4.707,0,3.832,6.011,3.1,11.632,2.448,16.767,1.5,24.36,1.015,28.156.82,30.711-.345,45.434-1.268,56.969,5.435,63.3c3.5,3.31,7.7,3.894,15.5,4.989a61.687,61.687,0,0,0,22.37-.973,115.036,115.036,0,0,0-2.5,13.9c-.413,3.577-.632,6.96-.7,10.075-.049,1.874-.049,3.65,0,5.354q1.639.292,3.206.584A208.717,208.717,0,0,1,75.727,106.1h0Q82.212,94.713,88.7,83.349a58.406,58.406,0,0,0,13.165,10.44c1.822,1.046,3.57,2.069,5.295,2.969a37.7,37.7,0,0,0,6.631,2.872,16.512,16.512,0,0,0,7.53.633c9.448-1.509,14.938-11.827,22.006-25.114A124.134,124.134,0,0,0,152.65,52.37,10.412,10.412,0,0,1,151.581,52.053Z" transform="translate(0.127 0.024)" fill="#5f2ce6"/>
|
||||
<g id="pattern" transform="translate(3.4 14.115)">
|
||||
<path id="Path-33" data-name="Path" d="M4.367,4.624A3.672,3.672,0,0,1,.068,1.7,3.416,3.416,0,0,1,.044.487C2.327.243,4.732.073,7.209,0l.073.292A3.709,3.709,0,0,1,4.367,4.624Z" transform="translate(39.401 26.988)" fill="#00b657"/>
|
||||
<ellipse id="Oval-4" data-name="Oval" cx="3.668" cy="3.675" rx="3.668" ry="3.675" transform="translate(0.024 32.171)" fill="#ffe323"/>
|
||||
<path id="Path-34" data-name="Path" d="M4.343.073c-.17,2.361-.291,4.794-.413,7.276A3.678,3.678,0,0,1,2.959.073,3.316,3.316,0,0,1,4.343.073Z" transform="translate(13.145 0.097)" fill="#00b657"/>
|
||||
<ellipse id="Oval-5" data-name="Oval" cx="3.668" cy="3.675" rx="3.668" ry="3.675" transform="translate(59.411 60.425)" fill="#00b657"/>
|
||||
<ellipse id="Oval-6" data-name="Oval" cx="3.668" cy="3.675" rx="3.668" ry="3.675" transform="translate(80.736 45.604)" fill="#ffe323"/>
|
||||
<path id="Path-35" data-name="Path" d="M6.631,5.665A35.97,35.97,0,0,1,0,2.794a3.667,3.667,0,0,1,7.165.195A3.566,3.566,0,0,1,6.631,5.665Z" transform="translate(103.908 79.946)" fill="#00b657"/>
|
||||
<path id="Path-36" data-name="Path" d="M3.243,5.953C2.2,5.758,1.105,5.564.036,5.369c-.049-1.7-.049-3.48,0-5.354A3.682,3.682,0,0,1,3.971,2.984,3.736,3.736,0,0,1,3.243,5.953Z" transform="translate(36.81 77.225)" fill="#00b657"/>
|
||||
<path id="Path-37" data-name="Path" d="M4.1,7.282A3.657,3.657,0,0,1,0,5.018C.972,3.558,1.967,2.025,2.939.468c.1-.146.194-.316.291-.462A3.654,3.654,0,0,1,7.019,2.974,3.715,3.715,0,0,1,4.1,7.282Z" transform="translate(124.359 47.23)" fill="#ffe323"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="hands" transform="translate(18.46)">
|
||||
<path id="Path-38" data-name="Path" d="M34.38,30.174c-5.222-1.509-10.444-3.042-15.691-4.551a12.349,12.349,0,0,1-1.773-9.661,5.146,5.146,0,0,0-.607-.268c-6.267-1.606-11.416,1.387-12.727-.852a1.811,1.811,0,0,1-.243-1.1A2.067,2.067,0,0,1,1.2,13.261,1.844,1.844,0,0,1,.764,12.02c.1-.973,1.287-1.679,2.769-2.239-1.6.146-2.745.1-3.279-.8a1.7,1.7,0,0,1-.194-1.29C.521,6.131,3.8,5.863,6.4,5.668a22.339,22.339,0,0,1,7.894.487,22.325,22.325,0,0,1,5.514,2.263c.607.341,1.19.681,1.7,1-.534-1.825-1.069-3.65-1.6-5.5-2.55.1-4.761.122-6.534.122-2.623,0-4.323-.1-4.882-1.144a1.445,1.445,0,0,1-.17-.973C8.561,1.093,9.872.534,15.993.193,19.733,0,21.579-.1,22.041.144c1.652.876,1.992,2.482,4.3,9.685A146.583,146.583,0,0,1,34.38,30.174Z" transform="translate(0.159 0.196)" fill="#ff8f6d"/>
|
||||
<g id="Group-3" data-name="Group" transform="translate(139.175 48.671)">
|
||||
<path id="Path-39" data-name="Path" d="M21.4,4.526l-.316-.049-4.372-.706L15.663,3.6l-2.72-.438L9.178,2.555H9.1l-4.542-.73c-.024.024-.024.073-.049.1h0A.532.532,0,0,0,4.441,1.8h0C3.737.754,3.081.024,2.183,0A2.348,2.348,0,0,0,.215,1.119C-.076,1.63-.514,2.945,2.28,7.082c-.874,2.336-1.166,4.064-.316,4.891a1.738,1.738,0,0,0,.9.438c.729.073,1.506-.487,2.235-1.411,0,.024-.024.049-.024.073-.389,1.192-.461,2.117.024,2.7a1.594,1.594,0,0,0,.874.487,1.594,1.594,0,0,0,1.239-.438,4.866,4.866,0,0,0,.389-.341,53.529,53.529,0,0,0,4.275,3.845c.024.17.049.316.073.487,4.178,1.509,8.331,2.993,12.509,4.5C22.415,11.292,21.419,4.891,21.4,4.526Z" transform="translate(1.461 6.668)" fill="#ff8f6d"/>
|
||||
<path id="Path-40" data-name="Path" d="M5.986,5.116c.68-.1,1.336-.219,1.919-.341-.121.17-.243.365-.389.584l5.708.925,3.668.584.1.024.146.024,3.449.56,1.8.292c-.413-2.142-.972-3.407-2.137-4.332a20.049,20.049,0,0,0-3.911-1.7C13.418.614,11.961.054,10.431.005c-1.36-.049-.972.243-5.174,1C2.076,1.563.545,1.611.108,2.658A1.9,1.9,0,0,0,.254,4.215C.983,5.481,3.581,5.481,5.986,5.116Z" transform="translate(0.013 0.117)" fill="#ff8f6d"/>
|
||||
</g>
|
||||
<path id="Path-41" data-name="Path" d="M14.768,2.713a18.042,18.042,0,0,0-6.558-1.7A18.444,18.444,0,0,0,.364,2.226L0,1.326A19.561,19.561,0,0,1,8.258.036a18.934,18.934,0,0,1,6.9,1.8Z" transform="translate(3.522 8.238)"/>
|
||||
<path id="Path-42" data-name="Path" d="M.68,3.069,0,2.363C.194,2.168,4.809-2.09,14.646,1.317l-.316.925C5.052-.971.729,3.044.68,3.069Z" transform="translate(3.158 11.24)"/>
|
||||
<path id="Path-43" data-name="Path" d="M8.647,8.688A23.552,23.552,0,0,1,2.38,3.723,23.878,23.878,0,0,1,0,.511L.826,0A21.831,21.831,0,0,0,3.109,3.066a21.767,21.767,0,0,0,6.024,4.77Z" transform="translate(144.106 55.29)"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -3,12 +3,6 @@ import * as Yup from 'yup';
|
||||
import { useFormik } from 'formik';
|
||||
import { Button, Intent } from '@blueprintjs/core';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { pick } from 'lodash';
|
||||
|
||||
import AppToaster from 'components/AppToaster';
|
||||
import ErrorMessage from 'components/ErrorMessage';
|
||||
|
||||
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
|
||||
import { MeteredBillingTabs, PaymentMethodTabs } from './SubscriptionTabs';
|
||||
import withBillingActions from './withBillingActions';
|
||||
@@ -21,13 +15,6 @@ function BillingForm({
|
||||
//#withBillingActions
|
||||
requestSubmitBilling,
|
||||
}) {
|
||||
// const defaultPlan = useMemo(() => ({
|
||||
// plan_slug: [
|
||||
// { id: 0, name: 'Basic', value: 'basic' },
|
||||
// { id: 0, name: 'Pro', value: 'pro' },
|
||||
// ],
|
||||
// }));
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -43,7 +30,7 @@ function BillingForm({
|
||||
|
||||
const initialValues = useMemo(
|
||||
() => ({
|
||||
plan_slug: 'basic',
|
||||
plan_slug: 'free',
|
||||
license_code: '',
|
||||
}),
|
||||
[],
|
||||
@@ -55,16 +42,9 @@ function BillingForm({
|
||||
initialValues: {
|
||||
...initialValues,
|
||||
},
|
||||
|
||||
onSubmit: (values, { setSubmitting, resetForm, setErrors }) => {
|
||||
requestSubmitBilling(values)
|
||||
.then((response) => {
|
||||
AppToaster.show({
|
||||
message: formatMessage({
|
||||
id: 'the_biling_has_been_successfully_created',
|
||||
}),
|
||||
intent: Intent.SUCCESS,
|
||||
});
|
||||
setSubmitting(false);
|
||||
})
|
||||
.catch((errors) => {
|
||||
@@ -72,7 +52,7 @@ function BillingForm({
|
||||
});
|
||||
},
|
||||
});
|
||||
console.log(formik.values, 'formik');
|
||||
|
||||
return (
|
||||
<div className={'billing-form'}>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
|
||||
@@ -1,162 +1,14 @@
|
||||
import React, {
|
||||
useState,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import { PaymentMethodTabs } from './SubscriptionTabs';
|
||||
import React from 'react';
|
||||
import BillingPlans from 'containers/Subscriptions/billingPlans';
|
||||
import BillingPeriods from 'containers/Subscriptions/billingPeriods';
|
||||
import { BillingPaymentmethod } from 'containers/Subscriptions/billingPaymentmethod';
|
||||
|
||||
function BillingTab({ formik }) {
|
||||
const [plan, setPlan] = useState();
|
||||
const planRef = useRef(null);
|
||||
const billingRef = useRef(null);
|
||||
|
||||
const handlePlan = () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const handleBilling = () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
handlePlan();
|
||||
handleBilling();
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section>
|
||||
<h1 className={'bg-title'}>
|
||||
<T id={'a_select_a_plan'} />
|
||||
</h1>
|
||||
<p className={'bg-message '}>
|
||||
<T id={'please_enter_your_preferred_payment_method'} />
|
||||
</p>
|
||||
<div className={'billing-form__plan-container'} ref={planRef}>
|
||||
<a
|
||||
id={'basic-plan'}
|
||||
className={`plan-wrapper plan-selected`}
|
||||
onClick={() =>
|
||||
setPlan({ ...formik.setFieldValue('plan_slug', 'basic') })
|
||||
}
|
||||
>
|
||||
<div className={'plan-header'}>
|
||||
<div className={'plan-name'}>
|
||||
<T id={'Basic'} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={'plan-description'}>
|
||||
<ul>
|
||||
<li>Sales/purchases module.</li>
|
||||
<li>Expense module.</li>
|
||||
<li>Inventory module.</li>
|
||||
<li>Unlimited status pages.</li>
|
||||
<li>Unlimited status pages.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={'plan-price'}>
|
||||
<span className={'amount'}>1200 LYD</span>
|
||||
<span className={'period'}>
|
||||
<T id={'year_per'} />
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
id={'pro-plan'}
|
||||
className={`plan-wrapper`}
|
||||
onClick={() =>
|
||||
setPlan({ ...formik.setFieldValue('plan_slug', 'pro') })
|
||||
}
|
||||
>
|
||||
<div className={'plan-header'}>
|
||||
<div className={'plan-name'}>
|
||||
<T id={'pro'} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={'plan-description'}>
|
||||
<ul>
|
||||
<li>Sales/purchases module.</li>
|
||||
<li>Expense module.</li>
|
||||
<li>Inventory module.</li>
|
||||
<li>Unlimited status pages.</li>
|
||||
<li>Unlimited status pages.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={'plan-price'}>
|
||||
<span className={'amount'}>1200 LYD</span>
|
||||
<span className={'period'}>
|
||||
<T id={'year_per'} />
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h1 className={'bg-title'}>
|
||||
<T id={'b_choose_your_billing'} />
|
||||
</h1>
|
||||
<p className={'bg-message'}>
|
||||
<T id={'please_enter_your_preferred_payment_method'} />
|
||||
</p>
|
||||
<div className={'payment-method-continer'} ref={billingRef}>
|
||||
<a
|
||||
href={'#'}
|
||||
id={'monthly'}
|
||||
className={'period-container billing-selected'}
|
||||
>
|
||||
<span className={'bg-period'}>
|
||||
<T id={'monthly'} />
|
||||
</span>
|
||||
<div className={'plan-price'}>
|
||||
<span className={'amount'}>1200 LYD</span>
|
||||
<span className={'period'}>
|
||||
<T id={'year'} />
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href={'#'} id={'yearly'} className={'period-container'}>
|
||||
<span className={'bg-period'}>
|
||||
<T id={'yearly'} />
|
||||
</span>
|
||||
<div className={'plan-price'}>
|
||||
<span className={'amount'}>1200 LYD</span>
|
||||
<span className={'period'}>
|
||||
<T id={'year'} />
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<h1 className={'bg-title'}>
|
||||
<T id={'c_payment_methods'} />
|
||||
</h1>
|
||||
<p className={'bg-message'}>
|
||||
<T id={'please_enter_your_preferred_payment_method'} />
|
||||
</p>
|
||||
<PaymentMethodTabs formik={formik} />
|
||||
</section>
|
||||
<BillingPlans title={'a_select_a_plan'} formik={formik} />
|
||||
<BillingPeriods title={'b_choose_your_billing'} formik={formik} />
|
||||
<BillingPaymentmethod title={'c_payment_methods'} formik={formik} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ function LicenseTab({
|
||||
<h4>
|
||||
<T id={'license_code'} />
|
||||
</h4>
|
||||
<p className={'bg-message'}>
|
||||
<p className="paragraph">
|
||||
<T id={'cards_will_be_charged'} />
|
||||
</p>
|
||||
|
||||
|
||||
17
client/src/containers/Subscriptions/billingPaymentmethod.js
Normal file
17
client/src/containers/Subscriptions/billingPaymentmethod.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { FormattedMessage as T, useIntl } from 'react-intl';
|
||||
import { PaymentMethodTabs } from './SubscriptionTabs';
|
||||
|
||||
export const BillingPaymentmethod = ({ formik, title }) => {
|
||||
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>
|
||||
<PaymentMethodTabs formik={formik} />
|
||||
</section>
|
||||
);
|
||||
};
|
||||
69
client/src/containers/Subscriptions/billingPeriods.js
Normal file
69
client/src/containers/Subscriptions/billingPeriods.js
Normal file
@@ -0,0 +1,69 @@
|
||||
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;
|
||||
89
client/src/containers/Subscriptions/billingPlans.js
Normal file
89
client/src/containers/Subscriptions/billingPlans.js
Normal file
@@ -0,0 +1,89 @@
|
||||
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;
|
||||
|
||||
22
client/src/containers/Subscriptions/withSubscriptions.js
Normal file
22
client/src/containers/Subscriptions/withSubscriptions.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
isSubscriptionOnTrialFactory,
|
||||
isSubscriptionInactiveFactory,
|
||||
isSubscriptionActiveFactory,
|
||||
} from 'store/subscription/subscription.selectors';
|
||||
|
||||
export default (mapState, slug) => {
|
||||
const isSubscriptionOnTrial = isSubscriptionOnTrialFactory(slug);
|
||||
const isSubscriptionInactive = isSubscriptionInactiveFactory(slug);
|
||||
const isSubscriptionActive = isSubscriptionActiveFactory(slug);
|
||||
|
||||
const mapStateToProps = (state, props) => {
|
||||
const mapped = {
|
||||
isSubscriptionOnTrial: isSubscriptionOnTrial(state, props),
|
||||
isSubscriptionInactive: isSubscriptionInactive(state, props),
|
||||
isSubscriptionActive: isSubscriptionActive(state, props),
|
||||
};
|
||||
return (mapState) ? mapState(mapped, state, props) : mapped;
|
||||
};
|
||||
return connect(mapStateToProps);
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
fetchSubscriptions,
|
||||
} from 'store/subscription/subscription.actions'
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
requestFetchSubscriptions: () => dispatch(fetchSubscriptions()),
|
||||
});
|
||||
|
||||
export default connect(null, mapDispatchToProps);
|
||||
@@ -53,10 +53,10 @@ export default {
|
||||
you_remembered_your_password: 'You remembered your password ?',
|
||||
new_password: 'New Password',
|
||||
submit_new_password: 'Submit new password',
|
||||
reset_your_password: 'Reset Your Password',
|
||||
we_ll_send_you_a_link_to_reset_your_password:
|
||||
'Enter your email address and we’ll send you a link to reset your password.',
|
||||
send_password_reset_link: 'Send password reset link',
|
||||
you_can_t_login: 'You can’t login?',
|
||||
we_ll_send_a_recovery_link_to_your_email:
|
||||
'We’ll send a recovery link to your email.',
|
||||
send_reset_password_mail: 'Send Reset Password Mail',
|
||||
return_to_log_in: 'Return to log in',
|
||||
sub_account: 'Sub account?',
|
||||
account_type: 'Account Type',
|
||||
@@ -398,7 +398,7 @@ export default {
|
||||
cost_account_: 'Cost account',
|
||||
inventory_account_: 'Inventory account',
|
||||
view_name_: 'View name',
|
||||
time_zone: 'Time zone',
|
||||
time_zone_: 'Time zone',
|
||||
location: 'Location',
|
||||
the_items_has_been_successfully_deleted:
|
||||
'The items have been successfully deleted.',
|
||||
@@ -704,12 +704,12 @@ export default {
|
||||
pro: 'PRO',
|
||||
monthly: 'Monthly',
|
||||
yearly: 'Yearly',
|
||||
license_code: 'License code',
|
||||
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.',
|
||||
'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',
|
||||
year_per: 'year',
|
||||
@@ -740,4 +740,30 @@ export default {
|
||||
sell_account: 'Sell Account',
|
||||
cost_account: 'Cost Account',
|
||||
inventory_account: 'Inventory 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: 'Let’s 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 ',
|
||||
base_currency: 'Base Currency',
|
||||
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',
|
||||
getting_started: 'Getting started',
|
||||
payment_or_trial: 'Payment or trial',
|
||||
initializing: 'Initializing',
|
||||
fiscal_year_: 'Fiscal year',
|
||||
welcome: 'Welcome ',
|
||||
sign_out: 'Sign out',
|
||||
we_re_here_to_help: 'We’re Here to Help!',
|
||||
date_start_: 'Date start',
|
||||
something_wentwrong: 'Something went wrong.',
|
||||
new_password: 'New password',
|
||||
license_code_: 'License code',
|
||||
legal_organization_name: 'Legal Organization Name'
|
||||
};
|
||||
|
||||
@@ -9,12 +9,6 @@ export default [
|
||||
loader: () => import('containers/Authentication/Login'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/register`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Authentication/Register'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/send_reset_password`,
|
||||
component: LazyLoader({
|
||||
@@ -33,4 +27,10 @@ export default [
|
||||
loader: () => import('containers/Authentication/InviteAccept'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `${BASE_URL}/register`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Authentication/Register'),
|
||||
}),
|
||||
}
|
||||
];
|
||||
|
||||
23
client/src/routes/register.js
Normal file
23
client/src/routes/register.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import LazyLoader from 'components/LazyLoader';
|
||||
|
||||
export default [
|
||||
|
||||
{
|
||||
path: '/register/subscription',
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Authentication/Register/RegisterSubscriptionForm'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: '/register/organization',
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Authentication/Register/RegisterOrganizationForm'),
|
||||
}),
|
||||
},
|
||||
{
|
||||
path: `/`,
|
||||
component: LazyLoader({
|
||||
loader: () => import('containers/Authentication/Register/RegisterUserForm'),
|
||||
}),
|
||||
},
|
||||
];
|
||||
@@ -4,7 +4,9 @@ import t from 'store/types';
|
||||
const initialState = {
|
||||
token: '',
|
||||
organization: '',
|
||||
organizationId: null,
|
||||
user: '',
|
||||
tenant: {},
|
||||
locale: '',
|
||||
errors: [],
|
||||
};
|
||||
@@ -15,6 +17,8 @@ export default createReducer(initialState, {
|
||||
state.token = token;
|
||||
state.user = user;
|
||||
state.organization = tenant.organization_id;
|
||||
state.organizationId = tenant.id;
|
||||
state.tenant = tenant;
|
||||
},
|
||||
|
||||
[t.LOGIN_FAILURE]: (state, action) => {
|
||||
@@ -36,3 +40,9 @@ export const isAuthenticated = (state) => !!state.authentication.token;
|
||||
export const hasErrorType = (state, errorType) => {
|
||||
return state.authentication.errors.find((e) => e.type === errorType);
|
||||
};
|
||||
|
||||
export const isTenantSeeded = (state) => !!state.tenant.seeded_at;
|
||||
export const isTenantBuilt = (state) => !!state.tenant.initialized_at;
|
||||
|
||||
export const isTenantHasSubscription = () => false;
|
||||
export const isTenantSubscriptionExpired = () => false;
|
||||
@@ -4,7 +4,7 @@ import t from 'store/types';
|
||||
export const submitBilling = ({ form }) => {
|
||||
return (dispatch) =>
|
||||
new Promise((resolve, reject) => {
|
||||
ApiService.post('payment', form)
|
||||
ApiService.post('subscription/license/payment', form)
|
||||
.then((response) => {
|
||||
resolve(response);
|
||||
})
|
||||
|
||||
64
client/src/store/organizations/organizations.actions.js
Normal file
64
client/src/store/organizations/organizations.actions.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import ApiService from 'services/ApiService';
|
||||
import t from 'store/types';
|
||||
|
||||
export const fetchOrganizations = () => (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get('organization/all').then((response) => {
|
||||
dispatch({
|
||||
type: t.ORGANIZATIONS_LIST_SET,
|
||||
payload: {
|
||||
organizations: response.data.organizations,
|
||||
},
|
||||
});
|
||||
resolve(response)
|
||||
}).catch(error => { reject(error); });
|
||||
});
|
||||
|
||||
export const buildTenant = () => (dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const organizationId = getState().authentication.organizationId;
|
||||
|
||||
dispatch({
|
||||
type: t.SET_ORGANIZATION_INITIALIZING,
|
||||
payload: { organizationId }
|
||||
});
|
||||
ApiService.post(`organization/build`).then((response) => {
|
||||
resolve(response);
|
||||
dispatch({
|
||||
type: t.SET_ORGANIZATION_INITIALIZED,
|
||||
payload: { organizationId }
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error.response.data.errors || []);
|
||||
});
|
||||
});
|
||||
|
||||
export const seedTenant = () => (dispatch, getState) => new Promise((resolve, reject) => {
|
||||
const organizationId = getState().authentication.organizationId;
|
||||
|
||||
dispatch({
|
||||
type: t.SET_ORGANIZATION_SEEDING,
|
||||
payload: { organizationId }
|
||||
});
|
||||
ApiService.post(`organization/seed/`).then((response) => {
|
||||
resolve(response);
|
||||
dispatch({
|
||||
type: t.SET_ORGANIZATION_SEEDED,
|
||||
payload: { organizationId }
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error.response.data.errors || []);
|
||||
});
|
||||
});
|
||||
|
||||
export const setOrganizationSetupCompleted = (congrats) => (dispatch, getState) => {
|
||||
const organizationId = getState().authentication.organizationId;
|
||||
|
||||
dispatch({
|
||||
type: t.SET_ORGANIZATION_CONGRATS,
|
||||
payload: {
|
||||
organizationId,
|
||||
congrats
|
||||
},
|
||||
});
|
||||
};
|
||||
73
client/src/store/organizations/organizations.reducers.js
Normal file
73
client/src/store/organizations/organizations.reducers.js
Normal file
@@ -0,0 +1,73 @@
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import t from 'store/types';
|
||||
|
||||
const initialState = {
|
||||
data: {},
|
||||
byOrganizationId: {},
|
||||
};
|
||||
|
||||
const reducer = createReducer(initialState, {
|
||||
|
||||
[t.ORGANIZATIONS_LIST_SET]: (state, action) => {
|
||||
const { organizations } = action.payload;
|
||||
const _data = {};
|
||||
const _dataByOrganizationId = {};
|
||||
|
||||
organizations.forEach((organization) => {
|
||||
_data[organization.id] = organization;
|
||||
_dataByOrganizationId[organization.organization_id] = organization.id;
|
||||
});
|
||||
state.data = _data;
|
||||
state.byOrganizationId = _dataByOrganizationId;
|
||||
},
|
||||
|
||||
[t.SET_ORGANIZATION_SEEDING]: (state, action) => {
|
||||
const { organizationId } = action.payload;
|
||||
|
||||
state.data[organizationId] = {
|
||||
...(state.data[organizationId] || {}),
|
||||
is_seeding: true,
|
||||
};
|
||||
},
|
||||
|
||||
[t.SET_ORGANIZATION_SEEDED]: (state, action) => {
|
||||
const { organizationId } = action.payload;
|
||||
|
||||
state.data[organizationId] = {
|
||||
...(state.data[organizationId] || {}),
|
||||
is_seeding: false,
|
||||
seeded_at: new Date().toISOString(),
|
||||
is_ready: true,
|
||||
};
|
||||
},
|
||||
|
||||
[t.SET_ORGANIZATION_INITIALIZING]: (state, action) => {
|
||||
const { organizationId } = action.payload;
|
||||
|
||||
state.data[organizationId] = {
|
||||
...(state.data[organizationId] || {}),
|
||||
is_initializing: true,
|
||||
};
|
||||
},
|
||||
|
||||
[t.SET_ORGANIZATION_INITIALIZED]: (state, action) => {
|
||||
const { organizationId } = action.payload;
|
||||
|
||||
state.data[organizationId] = {
|
||||
...(state.data[organizationId] || {}),
|
||||
is_initializing: false,
|
||||
initialized_at: new Date().toISOString(),
|
||||
};
|
||||
},
|
||||
|
||||
[t.SET_ORGANIZATION_CONGRATS]: (state, action) => {
|
||||
const { organizationId, congrats } = action.payload;
|
||||
|
||||
state.data[organizationId] = {
|
||||
...(state.data[organizationId] || {}),
|
||||
is_congrats: !!congrats,
|
||||
};
|
||||
}
|
||||
})
|
||||
|
||||
export default reducer;
|
||||
57
client/src/store/organizations/organizations.selectors.js
Normal file
57
client/src/store/organizations/organizations.selectors.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
const organizationSelector = (state, props) => state.organizations.data[props.organizationId];
|
||||
|
||||
export const getOrganizationByIdFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => organization
|
||||
);
|
||||
|
||||
export const isOrganizationSeededFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
return !!organization?.seeded_at;
|
||||
},
|
||||
);
|
||||
|
||||
export const isOrganizationBuiltFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
return !!organization?.initialized_at;
|
||||
},
|
||||
);
|
||||
|
||||
export const isOrganizationInitializingFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
return organization?.is_initializing;
|
||||
},
|
||||
);
|
||||
|
||||
export const isOrganizationSeedingFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
return organization?.is_seeding;
|
||||
},
|
||||
);
|
||||
|
||||
export const isOrganizationReadyFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
return organization?.is_ready;
|
||||
},
|
||||
);
|
||||
|
||||
export const isOrganizationSubscribedFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
return organization?.subscriptions?.length > 0;
|
||||
}
|
||||
);
|
||||
|
||||
export const isOrganizationCongratsFactory = () => createSelector(
|
||||
organizationSelector,
|
||||
(organization) => {
|
||||
return !!organization?.is_congrats;
|
||||
}
|
||||
)
|
||||
12
client/src/store/organizations/organizations.types.js
Normal file
12
client/src/store/organizations/organizations.types.js
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
export default {
|
||||
ORGANIZATIONS_LIST_SET: 'ORGANIZATIONS_LIST_SET',
|
||||
|
||||
SET_ORGANIZATION_SEEDING: 'SET_ORGANIZATION_SEEDING',
|
||||
SET_ORGANIZATION_SEEDED: 'SET_ORGANIZATION_SEEDED',
|
||||
|
||||
SET_ORGANIZATION_INITIALIZED: 'SET_ORGANIZATION_INITIALIZED',
|
||||
SET_ORGANIZATION_INITIALIZING: 'SET_ORGANIZATION_INITIALIZING',
|
||||
|
||||
SET_ORGANIZATION_CONGRATS: 'SET_ORGANIZATION_CONGRATS'
|
||||
};
|
||||
@@ -25,9 +25,13 @@ import bills from './Bills/bills.reducer';
|
||||
import vendors from './vendors/vendors.reducer';
|
||||
import paymentReceives from './PaymentReceive/paymentReceive.reducer';
|
||||
import paymentMades from './PaymentMades/paymentMade.reducer';
|
||||
import organizations from './organizations/organizations.reducers';
|
||||
import subscriptions from './subscription/subscription.reducer';
|
||||
|
||||
export default combineReducers({
|
||||
authentication,
|
||||
organizations,
|
||||
subscriptions,
|
||||
dashboard,
|
||||
users,
|
||||
accounts,
|
||||
@@ -45,12 +49,11 @@ export default combineReducers({
|
||||
exchangeRates,
|
||||
globalErrors,
|
||||
customers,
|
||||
|
||||
salesEstimates,
|
||||
salesInvoices,
|
||||
salesReceipts,
|
||||
bills,
|
||||
vendors,
|
||||
paymentReceives,
|
||||
paymentMades
|
||||
paymentMades,
|
||||
});
|
||||
|
||||
14
client/src/store/subscription/subscription.actions.js
Normal file
14
client/src/store/subscription/subscription.actions.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import ApiService from 'services/ApiService';
|
||||
import t from 'store/types';
|
||||
|
||||
export const fetchSubscriptions = () => (dispatch) => new Promise((resolve, reject) => {
|
||||
ApiService.get('subscription').then((response) => {
|
||||
dispatch({
|
||||
type: t.SET_PLAN_SUBSCRIPTIONS_LIST,
|
||||
payload: {
|
||||
subscriptions: response.data.subscriptions,
|
||||
},
|
||||
});
|
||||
resolve(response);
|
||||
}).catch((error) => { reject(error); })
|
||||
});
|
||||
19
client/src/store/subscription/subscription.reducer.js
Normal file
19
client/src/store/subscription/subscription.reducer.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import t from 'store/types';
|
||||
|
||||
const initialState = {
|
||||
data: {},
|
||||
};
|
||||
|
||||
export default createReducer(initialState, {
|
||||
|
||||
[t.SET_PLAN_SUBSCRIPTIONS_LIST]: (state, action) => {
|
||||
const { subscriptions } = action.payload;
|
||||
const _data = {};
|
||||
|
||||
subscriptions.forEach((subscription) => {
|
||||
_data[subscription.id] = subscription;
|
||||
});
|
||||
state.data = _data;
|
||||
},
|
||||
});
|
||||
23
client/src/store/subscription/subscription.selectors.js
Normal file
23
client/src/store/subscription/subscription.selectors.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
|
||||
const subscriptionSelector = (slug) => (state, props) => {
|
||||
const subscriptions = Object.values(state.subscriptions.data);
|
||||
return subscriptions.find((subscription) => subscription.slug === slug);
|
||||
};
|
||||
|
||||
export const isSubscriptionOnTrialFactory = (slug) => createSelector(
|
||||
subscriptionSelector(slug),
|
||||
(subscription) => !!subscription?.on_trial,
|
||||
);
|
||||
|
||||
export const isSubscriptionActiveFactory = (slug) => createSelector(
|
||||
subscriptionSelector(slug),
|
||||
(subscription) => {
|
||||
return !!subscription?.active;
|
||||
}
|
||||
);
|
||||
|
||||
export const isSubscriptionInactiveFactory = (slug) => createSelector(
|
||||
subscriptionSelector(slug),
|
||||
(subscription) => !!subscription?.inactive,
|
||||
);
|
||||
4
client/src/store/subscription/subscription.types.js
Normal file
4
client/src/store/subscription/subscription.types.js
Normal file
@@ -0,0 +1,4 @@
|
||||
|
||||
export default {
|
||||
SET_PLAN_SUBSCRIPTIONS_LIST: 'SET_PLAN_SUBSCRIPTIONS_LIST',
|
||||
};
|
||||
@@ -24,6 +24,8 @@ import bills from './Bills/bills.type';
|
||||
import vendors from './vendors/vendors.types';
|
||||
import paymentReceives from './PaymentReceive/paymentReceive.type';
|
||||
import paymentMades from './PaymentMades/paymentMade.type';
|
||||
import organizations from './organizations/organizations.types';
|
||||
import subscription from './subscription/subscription.types';
|
||||
|
||||
export default {
|
||||
...authentication,
|
||||
@@ -52,4 +54,6 @@ export default {
|
||||
...bills,
|
||||
...paymentReceives,
|
||||
...paymentMades,
|
||||
...organizations,
|
||||
...subscription,
|
||||
};
|
||||
|
||||
@@ -63,12 +63,14 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
|
||||
@import 'pages/exchange-rate';
|
||||
@import 'pages/customer';
|
||||
@import 'pages/billing';
|
||||
@import 'pages/register-wizard-page';
|
||||
@import 'pages/register-organizaton';
|
||||
|
||||
// Views
|
||||
@import 'views/filter-dropdown';
|
||||
@import 'views/sidebar';
|
||||
@import 'pages/estimate';
|
||||
|
||||
|
||||
.App {
|
||||
min-width: 960px;
|
||||
}
|
||||
@@ -173,3 +175,24 @@ body.authentication {
|
||||
.bp3-popover.bp3-tooltip {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
|
||||
.bigcapital-loading{
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
|
||||
.center{
|
||||
width: auto;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.text{
|
||||
margin-top: 12px;
|
||||
opacity: 0.85;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -11,4 +11,9 @@ body{
|
||||
.divider{
|
||||
border-top: 1px solid #e8e8e8;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.paragraph{
|
||||
line-height: 1.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
@@ -1,30 +1,32 @@
|
||||
|
||||
|
||||
.authentication-insider{
|
||||
margin-top: 80px;
|
||||
.authentication-insider {
|
||||
width: 384px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 40px;
|
||||
padding-top: 80px;
|
||||
|
||||
&__logo-section{
|
||||
&__logo-section {
|
||||
text-align: center;
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
&__content{
|
||||
&__content {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&__footer{
|
||||
.auth-copyright{
|
||||
&__footer {
|
||||
.auth-copyright {
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
|
||||
.bp3-icon-bigcapital{
|
||||
.bp3-icon-bigcapital {
|
||||
margin-top: 9px;
|
||||
|
||||
svg{
|
||||
path{
|
||||
fill: #A3A3A3;
|
||||
svg {
|
||||
path {
|
||||
fill: #a3a3a3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,55 +34,79 @@
|
||||
}
|
||||
}
|
||||
|
||||
.authentication-page {
|
||||
|
||||
&__goto-bigcapital{
|
||||
.authTransition{
|
||||
|
||||
&-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 250ms ease-in-out;
|
||||
}
|
||||
|
||||
&-enter-done {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&-exit-active {
|
||||
opacity: 0.5;
|
||||
transition: opacity 250ms ease-in-out;
|
||||
}
|
||||
&-exit-active {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.authentication-page {
|
||||
&__goto-bigcapital {
|
||||
position: fixed;
|
||||
margin-top: 30px;
|
||||
margin-left: 30px;
|
||||
color: #777;
|
||||
}
|
||||
|
||||
|
||||
.bp3-input {
|
||||
min-height: 40px;
|
||||
border: 2px solid #E3E3E3;
|
||||
border: 1px solid #ced4da;
|
||||
}
|
||||
.bp3-form-group{
|
||||
.bp3-form-group {
|
||||
margin-bottom: 25px;
|
||||
|
||||
&.bp3-intent-danger{
|
||||
.bp3-input{
|
||||
border-color: #eea9a9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-form-group.has-password-revealer{
|
||||
|
||||
.bp3-label{
|
||||
.bp3-form-group.has-password-revealer {
|
||||
.bp3-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.password-revealer{
|
||||
.text{
|
||||
.password-revealer {
|
||||
.text {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-button.bp3-fill.bp3-intent-primary{
|
||||
.bp3-button.bp3-fill.bp3-intent-primary {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&__label-section{
|
||||
margin-bottom: 34px;
|
||||
&__label-section {
|
||||
margin-bottom: 30px;
|
||||
color: #555;
|
||||
|
||||
h3{
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
font-size: 28px;
|
||||
color: #444;
|
||||
font-size: 22px;
|
||||
color: #2d2b43;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
@@ -92,24 +118,22 @@
|
||||
|
||||
&__form-wrapper {
|
||||
width: 100%;
|
||||
max-width: 415px;
|
||||
padding: 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
&__footer-links{
|
||||
&__footer-links {
|
||||
padding: 9px;
|
||||
border-top: 1px solid #ddd;
|
||||
border-bottom: 1px solid #ddd;
|
||||
text-align: center;
|
||||
margin-bottom: 1.2rem;
|
||||
|
||||
a{
|
||||
a {
|
||||
color: #0052cc;
|
||||
}
|
||||
}
|
||||
|
||||
&__loading-overlay{
|
||||
&__loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -132,21 +156,25 @@
|
||||
// Login Form
|
||||
// ------------------------------
|
||||
.login-form {
|
||||
// width: 690px;
|
||||
// margin: 0px auto;
|
||||
// padding: 85px 50px;
|
||||
|
||||
.checkbox{
|
||||
&--remember-me{
|
||||
margin: -4px 0 28px 0px;
|
||||
.checkbox {
|
||||
&--remember-me {
|
||||
margin: -6px 0 26px 0px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register Form
|
||||
// Register form
|
||||
// ----------------------------
|
||||
.register-form {
|
||||
|
||||
&__agreement-section {
|
||||
margin-top: -10px;
|
||||
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
margin-top: -10px;
|
||||
@@ -165,10 +193,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Send reset password
|
||||
// ----------------------------
|
||||
.send-reset-password {
|
||||
.form-group--crediential {
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
// Invite form.
|
||||
// ----------------
|
||||
.invite-form {
|
||||
|
||||
&__statement-section {
|
||||
margin-top: -10px;
|
||||
|
||||
p {
|
||||
font-size: 13px;
|
||||
margin-bottom: 20px;
|
||||
@@ -176,8 +215,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
.authentication-page__loading-overlay{
|
||||
.authentication-page__loading-overlay {
|
||||
background: rgba(252, 253, 255, 0.9);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 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;
|
||||
margin-bottom: 30px;
|
||||
|
||||
.plan-wrapper {
|
||||
display: flex;
|
||||
@@ -34,20 +35,20 @@
|
||||
.plan-header {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.plan-name {
|
||||
background: #3657ff;
|
||||
border-radius: 3px;
|
||||
padding: 1px 8px 1px 8px;
|
||||
padding: 2px 8px 2px 8px;
|
||||
font-size: 13px;
|
||||
color: #fff;
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 16px;
|
||||
height: 21px;
|
||||
}
|
||||
.plan-description {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 2em;
|
||||
line-height: 1.8rem;
|
||||
|
||||
&.plan-description ul {
|
||||
list-style: none;
|
||||
@@ -76,8 +77,14 @@
|
||||
background-color: #fcfdff;
|
||||
}
|
||||
}
|
||||
|
||||
.paragraph + .payment-method-continer{
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.payment-method-continer {
|
||||
margin-bottom: 30px;
|
||||
|
||||
.period-container {
|
||||
display: inline-flex;
|
||||
background-color: #fcfdff;
|
||||
@@ -110,22 +117,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.bg-title {
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
line-height: normal;
|
||||
}
|
||||
.bg-message {
|
||||
margin-bottom: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.license-container {
|
||||
.bp3-form-group {
|
||||
margin-bottom: 20px;
|
||||
.bp3-label {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group-license_code{
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.bp3-form-content {
|
||||
.bp3-input-group {
|
||||
display: block;
|
||||
@@ -139,9 +136,11 @@
|
||||
}
|
||||
h4 {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: #444444;
|
||||
}
|
||||
p {
|
||||
margin-top: 15px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
83
client/src/style/pages/register-organizaton.scss
Normal file
83
client/src/style/pages/register-organizaton.scss
Normal file
@@ -0,0 +1,83 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.register-organizaton-form {
|
||||
width: 690px;
|
||||
margin: 0px auto;
|
||||
padding: 80px 50px;
|
||||
|
||||
.register-org-title {
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
font-size: 22px;
|
||||
font-weight: 400;
|
||||
color: #555555;
|
||||
line-height: 2em;
|
||||
}
|
||||
p {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
color: #888888;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bp3-form-group {
|
||||
.bp3-input-group {
|
||||
.bp3-input {
|
||||
position: relative;
|
||||
width: 619px;
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-group--base-currency,
|
||||
.form-group--language {
|
||||
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
|
||||
min-width: 300px;
|
||||
min-height: 38px;
|
||||
}
|
||||
}
|
||||
.form-group--language {
|
||||
margin-left: 18px;
|
||||
}
|
||||
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
|
||||
min-width: 619px;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.form-group--time-zone {
|
||||
.bp3-text-muted {
|
||||
color: #000000;
|
||||
}
|
||||
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
|
||||
background: #fff;
|
||||
box-shadow: 0 0 0 transparent;
|
||||
border: 1px solid #ced4da;
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.register-org-note {
|
||||
width: 618px;
|
||||
font-size: 14px;
|
||||
line-height: 2.7rem;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 3px solid #f5f5f5;
|
||||
}
|
||||
.register-org-button {
|
||||
.bp3-button {
|
||||
background-color: #0052cc;
|
||||
width: 174px;
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
319
client/src/style/pages/register-wizard-page.scss
Normal file
319
client/src/style/pages/register-wizard-page.scss
Normal file
@@ -0,0 +1,319 @@
|
||||
.setup-page {
|
||||
max-width: 1400px;
|
||||
|
||||
&__right-section {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 30%;
|
||||
height: 1px;
|
||||
min-width: 300px;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
h1,
|
||||
h3{
|
||||
font-weight: 500;
|
||||
color: #6b7382;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
width: 70%;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
&__left-section {
|
||||
position: fixed;
|
||||
background-color: #01115e;
|
||||
overflow: auto;
|
||||
z-index: 1;
|
||||
height: 100%;
|
||||
width: 30%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
max-width: 350px;
|
||||
min-width: 300px;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #ffffff;
|
||||
padding: 25px;
|
||||
margin: 0px auto;
|
||||
border: none;
|
||||
height: 100%;
|
||||
|
||||
&__logo {
|
||||
opacity: 0.65;
|
||||
margin-bottom: 60px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 14px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 16px;
|
||||
opacity: 0.75;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__organization {
|
||||
font-size: 16px;
|
||||
opacity: 0.75;
|
||||
|
||||
span > a {
|
||||
text-decoration: underline;
|
||||
color: #ffffff;
|
||||
margin-top: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 3px;
|
||||
width: 100px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
&__contact-info {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.75;
|
||||
padding-bottom: 5px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
&__links {
|
||||
text-align: left;
|
||||
opacity: 0.55;
|
||||
|
||||
> div {
|
||||
font-size: 13px;
|
||||
margin-right: 15px;
|
||||
display: inline;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setup-page-steps {
|
||||
&-container {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
padding: 50px 0 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
list-style-type: none;
|
||||
width: 25%;
|
||||
text-align: center;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
|
||||
&::before {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
content: '';
|
||||
line-height: 30px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 0 auto 10px auto;
|
||||
border-radius: 50%;
|
||||
background-color: #75859c;
|
||||
}
|
||||
|
||||
&::after {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: #75859c;
|
||||
top: 5px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setup-organization {
|
||||
width: 580px;
|
||||
margin: 0 auto;
|
||||
padding: 45px 0 20px;
|
||||
|
||||
&__title-wrap{
|
||||
margin-bottom: 32px;
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
color: #565e6c;
|
||||
}
|
||||
.paragraph{
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
&__form{
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-form-group {
|
||||
.bp3-input-group {
|
||||
.bp3-input {
|
||||
height: 38px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bp3-button:not([class*='bp3-intent-']):not(.bp3-minimal) {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.register-org-note{
|
||||
font-size: 13px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #e1e1e1;
|
||||
margin-bottom: 1.75rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.register-org-button {
|
||||
.bp3-button {
|
||||
background-color: #0052cc;
|
||||
min-width: 175px;
|
||||
height: 40px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Setup initializing form
|
||||
.setup-initializing-form {
|
||||
width: 95%;
|
||||
margin: 0 auto;
|
||||
padding: 16% 0 0;
|
||||
|
||||
.bp3-progress-bar {
|
||||
background: rgba(92, 112, 128, 0.2);
|
||||
border-radius: 40px;
|
||||
display: block;
|
||||
height: 6px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
|
||||
.bp3-progress-meter {
|
||||
background-color: #809cb3;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
text-align: center;
|
||||
margin-top: 35px;
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #454c59;
|
||||
margin-top: 0;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
width: 70%;
|
||||
margin: 0 auto;
|
||||
color: #2e4266;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setup-congrats{
|
||||
width: 600px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
padding-top: 80px;
|
||||
|
||||
&__page{
|
||||
|
||||
}
|
||||
|
||||
&__text{
|
||||
margin-top: 30px;
|
||||
|
||||
h1{
|
||||
color: #2D2B43;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.paragraph{
|
||||
font-size: 15px;
|
||||
opacity: 0.85;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.bp3-button{
|
||||
height: 38px;
|
||||
padding-left: 25px;
|
||||
padding-right: 25px;
|
||||
font-size: 15px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Inject, Service } from 'typedi';
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { Router, Request, Response, NextFunction } from 'express';
|
||||
import asyncMiddleware from "api/middleware/asyncMiddleware";
|
||||
import JWTAuth from 'api/middleware/jwtAuth';
|
||||
import TenancyMiddleware from 'api/middleware/TenancyMiddleware';
|
||||
@@ -27,12 +27,14 @@ export default class OrganizationController extends BaseController{
|
||||
router.use(JWTAuth);
|
||||
router.use(AttachCurrentTenantUser);
|
||||
router.use(TenancyMiddleware);
|
||||
router.use(SubscriptionMiddleware('main'));
|
||||
|
||||
|
||||
// Should to seed organization tenant be configured.
|
||||
router.use('/seed', SubscriptionMiddleware('main'));
|
||||
router.use('/seed', SettingsMiddleware);
|
||||
router.use('/seed', EnsureConfiguredMiddleware);
|
||||
|
||||
router.use('/build', SubscriptionMiddleware('main'));
|
||||
|
||||
router.post(
|
||||
'/build',
|
||||
asyncMiddleware(this.build.bind(this))
|
||||
@@ -41,6 +43,10 @@ export default class OrganizationController extends BaseController{
|
||||
'/seed',
|
||||
asyncMiddleware(this.seed.bind(this)),
|
||||
);
|
||||
router.get(
|
||||
'/all',
|
||||
asyncMiddleware(this.allOrganizations.bind(this)),
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -116,4 +122,21 @@ export default class OrganizationController extends BaseController{
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing all organizations that assocaited to the authorized user.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async allOrganizations(req: Request, res: Response, next: NextFunction) {
|
||||
const { user } = req;
|
||||
|
||||
try {
|
||||
const organizations = await this.organizationService.listOrganizations(user);
|
||||
return res.status(200).send({ organizations });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,17 @@
|
||||
import { Router } from 'express'
|
||||
import { Container, Service } from 'typedi';
|
||||
import { Router, Request, Response, NextFunction } from 'express'
|
||||
import { Container, Service, Inject } from 'typedi';
|
||||
import JWTAuth from 'api/middleware/jwtAuth';
|
||||
import TenancyMiddleware from 'api/middleware/TenancyMiddleware';
|
||||
import AttachCurrentTenantUser from 'api/middleware/AttachCurrentTenantUser';
|
||||
import PaymentViaLicenseController from 'api/controllers/Subscription/PaymentViaLicense';
|
||||
import SubscriptionService from 'services/Subscription/SubscriptionService';
|
||||
import asyncMiddleware from 'api/middleware/asyncMiddleware';
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionController {
|
||||
export default class SubscriptionController {
|
||||
@Inject()
|
||||
subscriptionService: SubscriptionService;
|
||||
|
||||
/**
|
||||
* Router constructor.
|
||||
*/
|
||||
@@ -19,6 +24,26 @@ export default class SubscriptionController {
|
||||
|
||||
router.use('/license', Container.get(PaymentViaLicenseController).router());
|
||||
|
||||
router.get('/',
|
||||
asyncMiddleware(this.getSubscriptions.bind(this))
|
||||
);
|
||||
return router;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all subscriptions of the authenticated user's tenant.
|
||||
* @param {Request} req
|
||||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
async getSubscriptions(req: Request, res: Response, next: NextFunction) {
|
||||
const { tenantId } = req;
|
||||
|
||||
try {
|
||||
const subscriptions = await this.subscriptionService.getSubscriptions(tenantId);
|
||||
return res.status(200).send({ subscriptions });
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ export default {
|
||||
type: 'string',
|
||||
// config: true,
|
||||
},
|
||||
{
|
||||
key: 'financial_date_start',
|
||||
type: 'string',
|
||||
},
|
||||
{
|
||||
key: 'language',
|
||||
type: 'string',
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import Container from 'typedi';
|
||||
import TenancyService from 'services/Tenancy/TenancyService'
|
||||
|
||||
exports.up = (knex) => {
|
||||
const tenancyService = Container.get(TenancyService);
|
||||
|
||||
@@ -87,9 +87,6 @@ export default class AuthenticationService implements IAuthenticationService {
|
||||
// Remove password property from user object.
|
||||
Reflect.deleteProperty(user, 'password');
|
||||
|
||||
// Remove id property from tenant object.
|
||||
Reflect.deleteProperty(tenant, 'id');
|
||||
|
||||
return { user, token, tenant };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { ServiceError } from 'exceptions';
|
||||
import { ITenant } from 'interfaces';
|
||||
import { ISystemService, ISystemUser, ITenant } from 'interfaces';
|
||||
import {
|
||||
EventDispatcher,
|
||||
EventDispatcherInterface,
|
||||
@@ -38,7 +38,7 @@ export default class OrganizationService {
|
||||
* @param {srting} organizationId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async build(organizationId: string): Promise<void> {
|
||||
public async build(organizationId: string): Promise<void> {
|
||||
const tenant = await this.getTenantByOrgIdOrThrowError(organizationId);
|
||||
this.throwIfTenantInitizalized(tenant);
|
||||
|
||||
@@ -69,7 +69,7 @@ export default class OrganizationService {
|
||||
* @param {number} organizationId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
async seed(organizationId: string): Promise<void> {
|
||||
public async seed(organizationId: string): Promise<void> {
|
||||
const tenant = await this.getTenantByOrgIdOrThrowError(organizationId);
|
||||
this.throwIfTenantSeeded(tenant);
|
||||
|
||||
@@ -91,6 +91,20 @@ export default class OrganizationService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing all associated organizations to the given user.
|
||||
* @param {ISystemUser} user -
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async listOrganizations(user: ISystemUser): Promise<ITenant[]> {
|
||||
this.logger.info('[organization] trying to list all organizations.', { user });
|
||||
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
const tenant = await tenantRepository.getById(user.tenantId);
|
||||
|
||||
return [tenant];
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws error in case the given tenant is undefined.
|
||||
* @param {ITenant} tenant
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Service, Inject } from 'typedi';
|
||||
import { Plan, Tenant } from 'system/models';
|
||||
import { Plan, PlanSubscription } from 'system/models';
|
||||
import Subscription from 'services/Subscription/Subscription';
|
||||
import LicensePaymentMethod from 'services/Payment/LicensePaymentMethod';
|
||||
import PaymentContext from 'services/Payment';
|
||||
@@ -29,7 +29,7 @@ export default class SubscriptionService {
|
||||
* @param {string} licenseCode
|
||||
* @return {Promise}
|
||||
*/
|
||||
async subscriptionViaLicense(
|
||||
public async subscriptionViaLicense(
|
||||
tenantId: number,
|
||||
planSlug: string,
|
||||
paymentModel?: ILicensePaymentModel,
|
||||
@@ -53,4 +53,15 @@ export default class SubscriptionService {
|
||||
tenantId, paymentModel
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all subscription of the given tenant.
|
||||
* @param {number} tenantId
|
||||
*/
|
||||
public async getSubscriptions(tenantId: number) {
|
||||
this.logger.info('[subscription] trying to get tenant subscriptions.', { tenantId });
|
||||
const subscriptions = await PlanSubscription.query().where('tenant_id', tenantId);
|
||||
|
||||
return subscriptions;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,20 @@ export default class Tenant extends BaseModel {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual attributes.
|
||||
*/
|
||||
static get virtualAttributes() {
|
||||
return ['isReady'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tenant is ready.
|
||||
*/
|
||||
get isReady() {
|
||||
return !!(this.initializedAt && this.seededAt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query modifiers.
|
||||
*/
|
||||
|
||||
@@ -63,11 +63,21 @@ export default class TenantRepository extends SystemRepository {
|
||||
|
||||
/**
|
||||
* Retrieve tenant details by the given tenant id.
|
||||
* @param {string} tenantId
|
||||
* @param {string} tenantId - Tenant id.
|
||||
*/
|
||||
getById(tenantId: number) {
|
||||
return this.cache.get(`tenant.id.${tenantId}`, () => {
|
||||
return Tenant.query().findById(tenantId);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve tenant details with associated subscriptions
|
||||
* and plans by the given tenant id.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
*/
|
||||
getByIdWithSubscriptions(tenantId: number) {
|
||||
return Tenant.query().findById(tenantId)
|
||||
.withGraphFetched('subscriptions.plan');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user