This commit is contained in:
elforjani3
2020-10-14 18:16:46 +02:00
70 changed files with 2656 additions and 421 deletions

View File

@@ -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",

View File

@@ -0,0 +1,8 @@
const CLASSES = {
DATATABLE_EDITOR: 'DATATABLE_EDITOR'
};
export {
CLASSES,
}

View File

View File

@@ -0,0 +1,16 @@
export const registerWizardSteps = [
{
label: 'payment_or_trial',
},
{
label: 'initializing',
},
{
label: 'getting_started',
},
{
label: 'Congratulations',
},
];

View 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',
},
];

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

@@ -22,7 +22,6 @@ import withAuthenticationActions from './withAuthenticationActions';
import { compose } from 'utils';
const ERRORS_TYPES = {
INVALID_DETAILS: 'INVALID_DETAILS',
USER_INACTIVE: 'USER_INACTIVE',

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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'} />}

View File

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

@@ -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 well send you a link to reset your password.',
send_password_reset_link: 'Send password reset link',
you_can_t_login: 'You cant login?',
we_ll_send_a_recovery_link_to_your_email:
'Well 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: 'Lets Get Started',
tell_the_system_a_little_bit_about_your_organization:
'Tell the system a little bit about your organization.',
organization_details: 'Organization details',
financial_starting_date: 'Financial starting date ',
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: 'Were 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'
};

View File

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

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

View File

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

View File

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

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

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

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

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

View File

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

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

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

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

View File

@@ -0,0 +1,4 @@
export default {
SET_PLAN_SUBSCRIPTIONS_LIST: 'SET_PLAN_SUBSCRIPTIONS_LIST',
};

View File

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

View File

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

View File

@@ -11,4 +11,9 @@ body{
.divider{
border-top: 1px solid #e8e8e8;
height: 1px;
}
.paragraph{
line-height: 1.5;
font-size: 14px;
}

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -25,6 +25,10 @@ export default {
type: 'string',
// config: true,
},
{
key: 'financial_date_start',
type: 'string',
},
{
key: 'language',
type: 'string',

View File

@@ -1,3 +1,5 @@
import Container from 'typedi';
import TenancyService from 'services/Tenancy/TenancyService'
exports.up = (knex) => {
const tenancyService = Container.get(TenancyService);

View File

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

View File

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

View File

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

View File

@@ -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.
*/

View File

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