fix(Setup): fix organization setup.

This commit is contained in:
a.bouhuolia
2021-03-20 18:59:40 +02:00
parent e801d5d618
commit 671af0daae
46 changed files with 517 additions and 445 deletions

View File

@@ -63,7 +63,6 @@
"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-content-loader": "^6.0.1",

View File

@@ -1,6 +1,6 @@
export default [
{ name: 'English', value: 'EN' },
{ name: 'Arabic', value: 'AR' },
{ name: 'English', value: 'en' },
{ name: 'Arabic', value: 'ar' },
];

View File

@@ -11,7 +11,7 @@ export default function BigcapitalLoading({ className }) {
return (
<div className={classNames('bigcapital-loading', className)}>
<div class="center">
<Icon icon="bigcapital" height={37} width={214} />
<Icon icon="bigcapital" height={37} width={228} />
</div>
</div>
);

View File

@@ -4,18 +4,17 @@ import { Formik } from 'formik';
import { useIntl } from 'react-intl';
import * as Yup from 'yup';
import { useHistory } from 'react-router-dom';
import Toaster from 'components/AppToaster';
import 'style/pages/Setup/PaymentViaVoucherDialog.scss';
import { usePaymentByVoucher } from 'hooks/query';
import { DialogContent } from 'components';
import PaymentViaLicenseForm from './PaymentViaVoucherForm';
import withDialogActions from 'containers/Dialog/withDialogActions';
import withBillingActions from 'containers/Subscriptions/withBillingActions';
import withSubscriptionsActions from 'containers/Subscriptions/withSubscriptionsActions';
import { compose } from 'utils';
import { Intent } from '@blueprintjs/core';
/**
* Payment via license dialog content.
@@ -26,30 +25,43 @@ function PaymentViaLicenseDialogContent({
// #withDialog
closeDialog,
// #withBillingActions
requestSubmitBilling,
// #withSubscriptionsActions
requestFetchSubscriptions,
}) {
const { formatMessage } = useIntl();
const history = useHistory();
// Payment via voucher
const {
mutateAsync: paymentViaVoucherMutate,
} = usePaymentByVoucher();
// Handle submit.
const handleSubmit = (values, { setSubmitting }) => {
const handleSubmit = (values, { setSubmitting, setErrors }) => {
setSubmitting(true);
requestSubmitBilling({ ...values, ...subscriptionForm })
.then(() => {
return requestFetchSubscriptions();
})
paymentViaVoucherMutate({ ...values })
.then(() => {
Toaster.show({
message: 'Payment has been done successfully.',
intent: Intent.SUCCESS,
});
return closeDialog('payment-via-voucher');
})
.then(() => {
history.push('initializing');
})
.catch(
({
response: {
data: { errors },
},
}) => {
if (errors.find((e) => e.type === 'LICENSE.CODE.IS.INVALID')) {
setErrors({
license_code: 'The license code is not valid, please try agin.',
});
}
},
)
.finally((errors) => {
setSubmitting(false);
});
@@ -57,17 +69,18 @@ function PaymentViaLicenseDialogContent({
// Initial values.
const initialValues = {
license_number: '',
license_code: '',
plan_slug: '',
period: '',
...subscriptionForm,
};
// Validation schema.
const validationSchema = Yup.object().shape({
license_number: Yup.string()
license_code: Yup.string()
.required()
.min(10)
.max(10)
.label(formatMessage({ id: 'license_number' })),
.label(formatMessage({ id: 'license_code' })),
});
return (
@@ -82,8 +95,4 @@ function PaymentViaLicenseDialogContent({
);
}
export default compose(
withDialogActions,
withBillingActions,
withSubscriptionsActions,
)(PaymentViaLicenseDialogContent);
export default compose(withDialogActions)(PaymentViaLicenseDialogContent);

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Button, FormGroup, InputGroup, Intent } from '@blueprintjs/core';
import { Form, FastField, ErrorMessage } from 'formik';
import { Form, FastField, ErrorMessage, useFormikContext } from 'formik';
import { FormattedMessage as T } from 'react-intl';
import { compose } from 'redux';
@@ -15,12 +15,12 @@ import withDialogActions from 'containers/Dialog/withDialogActions';
* Payment via license form.
*/
function PaymentViaLicenseForm({
// #ownProps
isSubmitting,
// #withDialogActions
closeDialog,
}) {
// Formik context.
const { isSubmitting } = useFormikContext();
const licenseNumberRef = useAutofocus();
// Handle close button click.
@@ -33,15 +33,17 @@ function PaymentViaLicenseForm({
<div className={CLASSES.DIALOG_BODY}>
<p>Please enter your preferred payment method below.</p>
<FastField name="license_number">
<FastField name="license_code">
{({ field, meta: { error, touched } }) => (
<FormGroup
label={<T id={'voucher_number'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="voucher_number" />}
helperText={<ErrorMessage name="license_code" />}
className={'form-group--voucher_number'}
>
<InputGroup
large={true}
intent={inputIntent({ error, touched })}
{...field}
inputRef={(ref) => (licenseNumberRef.current = ref)}
/>

View File

@@ -7,16 +7,14 @@ import withDialogRedux from 'components/DialogReduxConnect';
import { compose } from 'utils';
// Lazy loading the content.
const PaymentViaLicenseDialogContent = lazy(() => import('./PaymentViaVoucherDialogContent'));
const PaymentViaLicenseDialogContent = lazy(() =>
import('./PaymentViaVoucherDialogContent'),
);
/**
* Payment via license dialog.
*/
function PaymentViaLicenseDialog({
dialogName,
payload,
isOpen
}) {
function PaymentViaLicenseDialog({ dialogName, payload, isOpen }) {
return (
<Dialog
name={dialogName}
@@ -33,9 +31,7 @@ function PaymentViaLicenseDialog({
/>
</DialogSuspense>
</Dialog>
)
);
}
export default compose(
withDialogRedux(),
)(PaymentViaLicenseDialog);
export default compose(withDialogRedux())(PaymentViaLicenseDialog);

View File

@@ -1,17 +1,11 @@
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)),
setOrganizationSetupCompleted: (congrats) =>
dispatch(setOrganizationSetupCompleted(congrats)),
});
export default connect(null, mapDispatchToProps);
export default connect(null, mapDispatchToProps);

View File

@@ -1,53 +1,49 @@
import React, { useEffect } from 'react';
import { useQuery } from 'react-query';
import { withWizard } from 'react-albus'
import { ProgressBar, Intent } from '@blueprintjs/core';
import { useBuildTenant } from 'hooks/query';
import 'style/pages/Setup/Initializing.scss';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import { compose } from 'utils';
/**
* Setup initializing step form.
*/
function SetupInitializingForm({
// #withOrganizationActions
requestOrganizationBuild,
wizard: { next },
}) {
const { isSuccess } = useQuery(
['build-tenant'], () => requestOrganizationBuild(),
);
export default function SetupInitializingForm() {
const {
mutateAsync: buildTenantMutate,
isLoading,
isError,
} = useBuildTenant();
useEffect(() => {
if (isSuccess) {
next();
}
}, [isSuccess, next]);
buildTenantMutate();
}, [buildTenantMutate]);
return (
<div class="setup-initializing-form">
<ProgressBar intent={Intent.PRIMARY} value={null} />
{isLoading && <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>
{isLoading ? (
<>
<h1>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>
</>
) : isError ? (
<>
<h1>Something went wrong!</h1>
<p class="paragraph">Please refresh the page</p>
</>
) : (
<>
<h1>Waiting to redirect</h1>
<p class="paragraph">Refresh the page if redirect not worked.</p>
</>
)}
</div>
</div>
);
}
export default compose(
withOrganizationActions,
withWizard,
)(SetupInitializingForm);

View File

@@ -14,7 +14,7 @@ import classNames from 'classnames';
import { TimezonePicker } from '@blueprintjs/timezone';
import { FormattedMessage as T } from 'react-intl';
import { Col, Row, ListSelect } from 'components';
import { FieldRequiredHint, Col, Row, ListSelect } from 'components';
import {
momentFormatter,
tansformDateValue,
@@ -38,9 +38,10 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
</h3>
{/* ---------- Organization name ---------- */}
<FastField name={'name'}>
<FastField name={'organization_name'}>
{({ form, field, meta: { error, touched } }) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'legal_organization_name'} />}
className={'form-group--name'}
intent={inputIntent({ error, touched })}
@@ -55,6 +56,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
<FastField name={'financialDateStart'}>
{({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'financial_starting_date'} />}
intent={inputIntent({ error, touched })}
helperText={<ErrorMessage name="financialDateStart" />}
@@ -82,6 +84,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'base_currency'} />}
className={classNames(
'form-group--base-currency',
@@ -137,6 +140,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
selectedItemProp={'value'}
defaultText={<T id={'select_language'} />}
popoverProps={{ minimal: true }}
filterable={false}
/>
</FormGroup>
)}
@@ -147,6 +151,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
<FastField name={'fiscalYear'}>
{({ form: { setFieldValue }, field: { value }, meta: { error, touched } }) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'fiscal_year'} />}
className={classNames(
'form-group--fiscal_year',
@@ -167,6 +172,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
onItemSelect={(item) => {
setFieldValue('fiscalYear', item.value)
}}
filterable={false}
/>
</FormGroup>
)}
@@ -180,6 +186,7 @@ export default function SetupOrganizationForm({ isSubmitting, values }) {
meta: { error, touched },
}) => (
<FormGroup
labelInfo={<FieldRequiredHint />}
label={<T id={'time_zone'} />}
className={classNames(
'form-group--time-zone',

View File

@@ -3,49 +3,33 @@ import * as Yup from 'yup';
import { Formik } from 'formik';
import moment from 'moment';
import { FormattedMessage as T, useIntl } from 'react-intl';
import { snakeCase } from 'lodash';
import { withWizard } from 'react-albus';
import { useQuery } from 'react-query';
import 'style/pages/Setup/Organization.scss';
import SetupOrganizationForm from './SetupOrganizationForm';
import { useOrganizationSetup } from 'hooks/query';
import withSettingsActions from 'containers/Settings/withSettingsActions';
import withSettings from 'containers/Settings/withSettings';
import withOrganizationActions from 'containers/Organization/withOrganizationActions';
import {
compose,
transformToForm,
optionsMapToArray,
transfromToSnakeCase,
} from 'utils';
/**
* Setup organization form.
*/
function SetupOrganizationPage({
// #withSettingsActions
requestSubmitOptions,
requestFetchOptions,
// #withOrganizationActions
requestOrganizationSeed,
// #withSettings
organizationSettings,
wizard,
setOrganizationSetupCompleted,
}) {
const { formatMessage } = useIntl();
const fetchSettings = useQuery(['settings'], () => requestFetchOptions({}));
const { mutateAsync: organizationSetupMutate } = useOrganizationSetup();
// Validation schema.
const validationSchema = Yup.object().shape({
name: Yup.string()
organization_name: Yup.string()
.required()
.label(formatMessage({ id: 'organization_name_' })),
financialDateStart: Yup.date()
@@ -67,35 +51,21 @@ function SetupOrganizationPage({
// Initial values.
const defaultValues = {
name: '',
organization_name: '',
financialDateStart: moment(new Date()).format('YYYY-MM-DD'),
baseCurrency: '',
language: '',
language: 'en',
fiscalYear: '',
timeZone: '',
...organizationSettings,
};
const initialValues = {
...defaultValues,
/**
* We only care about the fields in the form. Previously unfilled optional
* values such as `notes` come back from the API as null, so remove those
* as well.
*/
...transformToForm(organizationSettings, defaultValues),
};
// Handle the form submit.
const handleSubmit = (values, { setSubmitting, setErrors }) => {
const options = optionsMapToArray(values).map((option) => {
return { ...option, key: snakeCase(option.key), group: 'organization' };
});
requestSubmitOptions({ options })
.then(() => {
return requestOrganizationSeed();
})
organizationSetupMutate({ ...transfromToSnakeCase(values) })
.then(() => {
return setOrganizationSetupCompleted(true);
})
@@ -132,8 +102,4 @@ function SetupOrganizationPage({
export default compose(
withSettingsActions,
withOrganizationActions,
withWizard,
withSettings(({ organizationSettings }) => ({
organizationSettings,
})),
)(SetupOrganizationPage);

View File

@@ -1,13 +1,9 @@
import React from 'react';
import { Wizard } from 'react-albus';
import { useHistory } from 'react-router-dom';
import withSubscriptions from 'containers/Subscriptions/withSubscriptions';
import SetupDialogs from './SetupDialogs';
import SetupWizardContent from './SetupWizardContent';
import withSubscriptions from 'containers/Subscriptions/withSubscriptions';
import withOrganization from 'containers/Organization/withOrganization';
import withCurrentOrganization from 'containers/Organization/withCurrentOrganization';
import withSetupWizard from '../../store/organizations/withSetupWizard';
@@ -24,37 +20,17 @@ function SetupRightSection({
isOrganizationSetupCompleted,
// #withSetupWizard
isCongratsStep,
isSubscriptionStep,
isInitializingStep,
isOrganizationStep,
setupStepId,
setupStepIndex,
// #withSubscriptions
isSubscriptionActive,
}) {
const history = useHistory();
const handleSkip = ({ step, push }) => {
const scenarios = [
{ condition: isCongratsStep, redirectTo: 'congrats' },
{ condition: isSubscriptionStep, redirectTo: 'subscription' },
{ condition: isInitializingStep, redirectTo: 'initializing' },
{ condition: isOrganizationStep, redirectTo: 'organization' },
];
const scenario = scenarios.find((scenario) => scenario.condition);
if (scenario) {
push(scenario.redirectTo);
}
};
return (
<section className={'setup-page__right-section'}>
<Wizard
onNext={handleSkip}
basename={'/setup'}
history={history}
render={SetupWizardContent}
<SetupWizardContent
setupStepId={setupStepId}
setupStepIndex={setupStepIndex}
/>
<SetupDialogs />
</section>
@@ -84,17 +60,8 @@ export default compose(
}),
'main',
),
withSetupWizard(
({
isCongratsStep,
isSubscriptionStep,
isInitializingStep,
isOrganizationStep,
}) => ({
isCongratsStep,
isSubscriptionStep,
isInitializingStep,
isOrganizationStep,
}),
),
withSetupWizard(({ setupStepId, setupStepIndex }) => ({
setupStepId,
setupStepIndex,
})),
)(SetupRightSection);

View File

@@ -0,0 +1,8 @@
import React from 'react';
export default function SetupSteps({ step, children }) {
const activeStep = React.Children.toArray(children).filter(
(child) => child.props.id === step.id,
);
return activeStep;
}

View File

@@ -1,21 +1,15 @@
import React from 'react';
import { Formik } from 'formik';
import { withWizard } from 'react-albus';
import 'style/pages/Setup/Subscription.scss';
import SetupSubscriptionForm from './SetupSubscriptionForm';
import { SubscriptionFormSchema } from './SubscriptionForm.schema';
import { compose } from 'utils';
/**
* Subscription step of wizard setup.
*/
function SetupSubscription({
// #withWizard
wizard,
}) {
export default function SetupSubscription() {
// Initial values.
const initialValues = {
plan_slug: 'free',
@@ -23,6 +17,7 @@ function SetupSubscription({
license_code: '',
};
// Handle form submit.
const handleSubmit = () => {};
return (
@@ -35,8 +30,4 @@ function SetupSubscription({
/>
</div>
);
}
export default compose(
withWizard,
)(SetupSubscription);
}

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { Steps, Step } from 'react-albus';
import { TransitionGroup, CSSTransition } from 'react-transition-group';
import SetupSteps from './SetupSteps';
import WizardSetupSteps from './WizardSetupSteps';
import SetupSubscription from './SetupSubscription';
@@ -12,37 +11,19 @@ import SetupCongratsPage from './SetupCongratsPage';
/**
* Setup wizard content.
*/
export default function SetupWizardContent({
step,
steps
}) {
export default function SetupWizardContent({ setupStepIndex, setupStepId }) {
return (
<div class="setup-page__content">
<WizardSetupSteps currentStep={steps.indexOf(step) + 1} />
<WizardSetupSteps currentStep={setupStepIndex} />
<TransitionGroup>
<CSSTransition key={step.id} timeout={{ enter: 500, exit: 500 }}>
<div class="setup-page-form">
<Steps key={step.id} step={step}>
<Step id="subscription">
<SetupSubscription />
</Step>
<Step id={'initializing'}>
<SetupInitializingForm />
</Step>
<Step id="organization">
<SetupOrganizationPage />
</Step>
<Step id="congrats">
<SetupCongratsPage />
</Step>
</Steps>
</div>
</CSSTransition>
</TransitionGroup>
<div class="setup-page-form">
<SetupSteps step={{ id: setupStepId }}>
<SetupSubscription id="subscription" />
<SetupInitializingForm id={'initializing'} />
<SetupOrganizationPage id="organization" />
<SetupCongratsPage id="congrats" />
</SetupSteps>
</div>
</div>
);
}

View File

@@ -1,22 +1,19 @@
import React from 'react';
import classNames from 'classnames';
import { FormattedMessage as T } from 'react-intl';
import { registerWizardSteps } from 'common/registerWizard'
import { registerWizardSteps } from 'common/registerWizard';
function WizardSetupStep({
label,
isActive = false
}) {
function WizardSetupStep({ label, isActive = false }) {
return (
<li className={classNames({ 'is-active': isActive })}>
<p className={'wizard-info'}><T id={label} /></p>
<p className={'wizard-info'}>
<T id={label} />
</p>
</li>
);
}
function WizardSetupSteps({
currentStep = 1,
}) {
export default function WizardSetupSteps({ currentStep = 1 }) {
return (
<div className={'setup-page-steps-container'}>
<div className={'setup-page-steps'}>
@@ -24,7 +21,7 @@ function WizardSetupSteps({
{registerWizardSteps.map((step, index) => (
<WizardSetupStep
label={step.label}
isActive={(index + 1) == currentStep}
isActive={index + 1 === currentStep}
/>
))}
</ul>
@@ -32,5 +29,3 @@ function WizardSetupSteps({
</div>
);
}
export default WizardSetupSteps;

View File

@@ -1,5 +1,5 @@
import { useMutation, useQueryClient } from 'react-query';
import { useQueryTenant, useRequestQuery } from '../useQueryRequest';
import { useRequestQuery } from '../useQueryRequest';
import useApiRequest from '../useRequest';
import t from './types';
@@ -24,9 +24,7 @@ export function useAccounts(query, props) {
[t.ACCOUNTS, query],
{ method: 'get', url: 'accounts', params: query },
{
select: (response) => {
return response.data.accounts;
},
select: (res) => res.data.accounts,
defaultData: [],
...props,
},

View File

@@ -1,4 +1,3 @@
import { useEffect } from 'react';
import { useMutation } from 'react-query';
import useApiRequest from '../useRequest';
import { useAuthActions } from '../state';
@@ -10,20 +9,16 @@ export const useAuthLogin = (props) => {
const { setLogin } = useAuthActions();
const apiRequest = useApiRequest();
const states = useMutation(
return useMutation(
(values) => apiRequest.post('auth/login', values),
{
select: (res) => res.data,
onSuccess: (data) => {
setLogin(data.data);
},
...props
}
);
const { isSuccess, data: response } = states;
useEffect(() => {
if (isSuccess) { setLogin(response.data); }
}, [isSuccess, response, setLogin]);
return states;
};
/**

View File

@@ -21,3 +21,5 @@ export * from './users';
export * from './invite';
export * from './exchangeRates';
export * from './contacts';
export * from './subscriptions';
export * from './organization';

View File

@@ -1,8 +1,8 @@
import { useMutation } from 'react-query';
import { useMutation, useQueryClient } from 'react-query';
import { batch } from 'react-redux';
import t from './types';
import useApiRequest from '../useRequest';
import { useRequestQuery } from '../useQueryRequest';
import { useEffect } from 'react';
import { useSetOrganizations, useSetSubscriptions } from '../state';
import { omit } from 'lodash';
@@ -33,29 +33,26 @@ export function useCurrentOrganization(props) {
const setOrganizations = useSetOrganizations();
const setSubscriptions = useSetSubscriptions();
const query = useRequestQuery(
return useRequestQuery(
[t.ORGANIZATION_CURRENT],
{ method: 'get', url: `organization/current` },
{
select: (res) => res.data.organization,
defaultData: {},
onSuccess: (data) => {
const organization = omit(data, ['subscriptions']);
batch(() => {
// Sets subscriptions.
setSubscriptions(data.subscriptions);
// Sets organizations.
setOrganizations([organization]);
});
},
...props,
},
);
useEffect(() => {
if (query.isSuccess) {
const organization = omit(query.data, ['subscriptions']);
// Sets organizations.
setOrganizations([organization]);
// Sets subscriptions.
setSubscriptions(query.data.subscriptions);
}
}, [query.data, query.isSuccess, setOrganizations, setSubscriptions]);
return query;
}
/**
@@ -63,30 +60,46 @@ export function useCurrentOrganization(props) {
*/
export function useBuildTenant(props) {
const apiRequest = useApiRequest();
const queryClient = useQueryClient();
return useMutation(
(values) => apiRequest.post('organization/build'),
{
onSuccess: (res, values) => {
},
...props,
return useMutation((values) => apiRequest.post('organization/build'), {
onSuccess: (res, values) => {
queryClient.invalidateQueries(t.ORGANIZATION_CURRENT);
queryClient.invalidateQueries(t.ORGANIZATIONS);
},
);
};
...props,
});
}
/**
* Seeds the current tenant
*/
export function useSeedTenant() {
const apiRequest = useApiRequest();
const queryClient = useQueryClient();
return useMutation((values) => apiRequest.post('organization/seed'), {
onSuccess: (res) => {
queryClient.invalidateQueries(t.ORGANIZATION_CURRENT);
queryClient.invalidateQueries(t.ORGANIZATIONS);
},
});
}
/**
* Organization setup.
*/
export function useOrganizationSetup() {
const apiRequest = useApiRequest();
const queryClient = useQueryClient();
return useMutation(
(values) => apiRequest.post('organization/seed'),
(values) => apiRequest.post(`setup/organization`, values),
{
onSuccess: (res) => {
queryClient.invalidateQueries(t.ORGANIZATION_CURRENT);
queryClient.invalidateQueries(t.ORGANIZATIONS);
},
}
)
};
},
);
}

View File

@@ -0,0 +1,44 @@
import { useEffect } from "react"
import { useMutation, useQueryClient } from "react-query";
import { useRequestQuery } from "../useQueryRequest";
import useApiRequest from "../useRequest";
import { useSetSubscriptions } from '../state/subscriptions';
import T from './types';
/**
* Subscription payment via voucher.
*/
export const usePaymentByVoucher = (props) => {
const apiRequest = useApiRequest();
const queryClient = useQueryClient();
return useMutation(
(values) => apiRequest.post('subscription/license/payment', values),
{
onSuccess: () => {
queryClient.invalidateQueries(T.SUBSCRIPTIONS);
queryClient.invalidateQueries(T.ORGANIZATION_CURRENT);
queryClient.invalidateQueries(T.ORGANIZATIONS);
},
...props,
}
);
}
/**
* Fetches the organization subscriptions.
*/
export const useOrganizationSubscriptions = (props) => {
const setSubscriptions = useSetSubscriptions();
const state = useRequestQuery(
[T.SUBSCRIPTIONS],
{ method: 'get', url: 'subscriptions' },
{ ...props },
);
useEffect(() => {
if (state.isSuccess) {
setSubscriptions(state.data);
}
}, [state.isSuccess, state.data, setSubscriptions])
};

View File

@@ -94,6 +94,10 @@ const ORGANIZATIONS = {
ORGANIZATION_CURRENT: 'ORGANIZATION_CURRENT',
};
const SUBSCRIPTIONS = {
SUBSCRIPTIONS: 'SUBSCRIPTIONS',
}
export default {
...ACCOUNTS,
...BILLS,
@@ -111,4 +115,5 @@ export default {
...USERS,
...SETTING,
...ORGANIZATIONS,
...SUBSCRIPTIONS
};

View File

@@ -11,4 +11,4 @@ export const useSetSubscriptions = () => {
return useCallback((subscriptions) => {
dispatch(setSubscriptions(subscriptions));
}, [dispatch]);
}
}

View File

@@ -1,7 +1,8 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { createTableStateReducers } from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -12,15 +13,21 @@ const initialState = {
const STORAGE_KEY = 'bigcapital:bills';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('BILLS'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
}
});
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
},
CONFIG,
reducerInstance,
);

View File

@@ -1,9 +1,10 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import {
createTableStateReducers,
} from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -12,17 +13,23 @@ const initialState = {
},
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('ESTIMATES'),
});
const STORAGE_KEY = 'bigcapital:estimates';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('ESTIMATES'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
});
export default persistReducer(
CONFIG,
reducerInstance,
);

View File

@@ -1,9 +1,10 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import {
createTableStateReducers,
} from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -12,17 +13,23 @@ const initialState = {
},
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('INVOICES'),
});
const STORAGE_KEY = 'bigcapital:invoices';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('INVOICES'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
}
});
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
},
CONFIG,
reducerInstance,
);

View File

@@ -1,9 +1,10 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import {
createTableStateReducers,
} from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -13,17 +14,20 @@ const initialState = {
},
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('PAYMENT_MADES'),
});
const STORAGE_KEY = 'bigcapital:paymentMades';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
}
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('PAYMENT_MADES'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
reducerInstance,
);
});
export default persistReducer(CONFIG, reducerInstance);

View File

@@ -1,9 +1,10 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import {
createTableStateReducers,
} from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -12,17 +13,23 @@ const initialState = {
},
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('PAYMENT_RECEIVES'),
});
const STORAGE_KEY = 'bigcapital:paymentReceives';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('PAYMENT_RECEIVES'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
});
export default persistReducer(
CONFIG,
reducerInstance,
);

View File

@@ -1,25 +1,27 @@
import { createReducer} from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import {
createTableStateReducers,
} from 'store/tableState.reducer';
import { createTableStateReducers } from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {},
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('ACCOUNTS'),
});
const STORAGE_KEY = 'bigcapital:accounts';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
},
reducerInstance,
);
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('ACCOUNTS'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
}
});
export default persistReducer(CONFIG, reducerInstance);

View File

@@ -1,5 +1,6 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import purgeStoredState from 'redux-persist/es/purgeStoredState';
import storage from 'redux-persist/lib/storage';
import t from 'store/types';
@@ -14,10 +15,16 @@ const initialState = {
};
const STORAGE_KEY = 'bigcapital:authentication';
const CONFIG = {
key: STORAGE_KEY,
blacklist: ['errors'],
storage,
};
const reducerInstance = createReducer(initialState, {
[t.LOGIN_SUCCESS]: (state, action) => {
const { token, user, tenant } = action.payload;
state.token = token;
state.user = user;
state.organization = tenant.organization_id;
@@ -32,14 +39,14 @@ const reducerInstance = createReducer(initialState, {
[t.LOGIN_CLEAR_ERRORS]: (state) => {
state.errors = [];
},
[t.RESET]: () => {
purgeStoredState(CONFIG);
}
});
export default persistReducer(
{
key: STORAGE_KEY,
blacklist: ['errors'],
storage,
},
CONFIG,
reducerInstance,
);

View File

@@ -1,6 +1,6 @@
import t from 'store/types';
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
const initialState = {
@@ -20,6 +20,12 @@ const initialState = {
const STORAGE_KEY = 'bigcapital:dashboard';
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['sidebarExpended', 'previousSidebarExpended'],
storage,
};
const reducerInstance = createReducer(initialState, {
[t.CHANGE_DASHBOARD_PAGE_TITLE]: (state, action) => {
state.pageTitle = action.pageTitle;
@@ -115,19 +121,13 @@ const reducerInstance = createReducer(initialState, {
const { backLink } = action.payload;
state.backLink = backLink;
},
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
});
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: [
'sidebarExpended',
'previousSidebarExpended',
],
storage,
},
reducerInstance,
);
export default persistReducer(CONFIG, reducerInstance);
export const getDialogPayload = (state, dialogName) => {
return typeof state.dashboard.dialogs[dialogName] !== 'undefined'

View File

@@ -1,7 +1,8 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { createTableStateReducers } from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -10,17 +11,20 @@ const initialState = {
},
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('EXPENSES'),
});
const STORAGE_KEY = 'bigcapital:expenses';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
},
reducerInstance,
);
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('EXPENSES'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
}
});
export default persistReducer(CONFIG, reducerInstance);

View File

@@ -1,7 +1,8 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { createTableStateReducers } from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -12,17 +13,20 @@ const initialState = {
selectedRows: [],
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('INVENTORY_ADJUSTMENTS'),
});
const STORAGE_KEY = 'bigcapital:inventoryAdjustments';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('INVENTORY_ADJUSTMENTS'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
reducerInstance,
);
});
export default persistReducer(CONFIG, reducerInstance);

View File

@@ -1,27 +1,33 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import {
createTableStateReducers,
} from 'store/tableState.reducer';
import t from 'store/types';
// Initial state.
const initialState = {
tableState: {},
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('ITEMS_CATEGORIES'),
});
const STORAGE_KEY = 'bigcapital:itemCategories';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('ITEMS_CATEGORIES'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
});
export default persistReducer(
CONFIG,
reducerInstance,
);

View File

@@ -1,7 +1,8 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { createTableStateReducers } from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -12,17 +13,23 @@ const initialState = {
selectedRows: [],
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('ITEMS'),
});
const STORAGE_KEY = 'bigcapital:items';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('ITEMS'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
});
export default persistReducer(
CONFIG,
reducerInstance,
);

View File

@@ -1,7 +1,8 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import { createTableStateReducers } from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -10,17 +11,20 @@ const initialState = {
},
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('MANUAL_JOURNALS'),
});
const STORAGE_KEY = 'bigcapital:manualJournals';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('MANUAL_JOURNALS'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
reducerInstance,
);
});
export default persistReducer(CONFIG, reducerInstance);

View File

@@ -14,7 +14,10 @@ const reducer = createReducer(initialState, {
const _dataByOrganizationId = {};
organizations.forEach((organization) => {
_data[organization.id] = organization;
_data[organization.id] = {
...state.data[organization.id],
...organization,
};
_dataByOrganizationId[organization.organization_id] = organization.id;
});
state.data = _data;

View File

@@ -6,16 +6,28 @@ export default (mapState) => {
isOrganizationSetupCompleted,
isOrganizationInitialized,
isOrganizationSeeded,
isSubscriptionActive
} = props;
const mapped = {
const condits = {
isCongratsStep: isOrganizationSetupCompleted,
isSubscriptionStep: !isSubscriptionActive,
isInitializingStep: isSubscriptionActive && !isOrganizationInitialized,
isOrganizationStep: isOrganizationInitialized && !isOrganizationSeeded,
};
const scenarios = [
{ condition: condits.isCongratsStep, step: 'congrats' },
{ condition: condits.isSubscriptionStep, step: 'subscription' },
{ condition: condits.isInitializingStep, step: 'initializing' },
{ condition: condits.isOrganizationStep, step: 'organization' },
];
const setupStep = scenarios.find((scenario) => scenario.condition);
const mapped = {
...condits,
setupStepId: setupStep?.step,
setupStepIndex: scenarios.indexOf(setupStep) ,
};
return mapState ? mapState(mapped, state, props) : mapped;
};
return connect(mapStateToProps);

View File

@@ -1,9 +1,10 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import {
createTableStateReducers,
} from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -11,18 +12,24 @@ const initialState = {
pageIndex: 0,
},
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('RECEIPTS'),
});
const STORAGE_KEY = 'bigcapital:receipts';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('RECEIPTS'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
},
});
export default persistReducer(
CONFIG,
reducerInstance,
);

View File

@@ -1,11 +1,11 @@
import { camelCase } from 'lodash';
import { createReducer } from '@reduxjs/toolkit';
import t from 'store/types';
import { optionsArrayToMap } from 'utils';
const initialState = {
data: {
organization: {
name: 'Bigcapital, Limited Liabilities',
name: 'Bigcapital, LLC',
},
manualJournals: {},
bills: {},

View File

@@ -21,4 +21,5 @@ export const setSubscriptions = (subscriptions) => {
subscriptions,
},
}
};
};

View File

@@ -1,9 +1,8 @@
import { createReducer } from '@reduxjs/toolkit';
import { persistReducer } from 'redux-persist';
import { persistReducer, purgeStoredState } from 'redux-persist';
import storage from 'redux-persist/lib/storage';
import {
createTableStateReducers,
} from 'store/tableState.reducer';
import { createTableStateReducers } from 'store/tableState.reducer';
import t from 'store/types';
const initialState = {
tableState: {
@@ -11,17 +10,21 @@ const initialState = {
pageIndex: 0,
},
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('VENDORS'),
});
const STORAGE_KEY = 'bigcapital:vendors';
export default persistReducer(
{
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
},
reducerInstance,
);
const CONFIG = {
key: STORAGE_KEY,
whitelist: ['tableState'],
storage,
};
const reducerInstance = createReducer(initialState, {
...createTableStateReducers('VENDORS'),
[t.RESET]: () => {
purgeStoredState(CONFIG);
}
});
export default persistReducer(CONFIG, reducerInstance);

View File

@@ -13,7 +13,7 @@
color: #565e6c;
}
.paragraph {
opacity: 0.75;
opacity: 0.8;
}
}
@@ -38,6 +38,10 @@
}
}
label.bp3-label{
color: #313744;
}
.form-group--language {
margin-left: 10px;
}

View File

@@ -30,7 +30,7 @@
font-weight: 600;
}
&__period{
color: #666;
color: #2f3863;
font-size: 14px;
font-weight: 500;

View File

@@ -60,7 +60,7 @@
}
&__period {
font-weight: 400;
color: #666;
color: #2f3863;
&::before {
content: '/';

View File

@@ -8,8 +8,10 @@ import {
NoPaymentModelWithPricedPlan,
PaymentAmountInvalidWithPlan,
PaymentInputInvalid,
VoucherCodeRequired
} from 'exceptions';
import { ILicensePaymentModel } from 'interfaces';
import instance from 'tsyringe/dist/typings/dependency-container';
@Service()
export default class PaymentViaLicenseController extends PaymentMethodController {
@@ -67,6 +69,11 @@ export default class PaymentViaLicenseController extends PaymentMethodController
} catch (exception) {
const errorReasons = [];
if (exception instanceof VoucherCodeRequired) {
errorReasons.push({
type: 'VOUCHER_CODE_REQUIRED', code: 100,
});
}
if (exception instanceof NoPaymentModelWithPricedPlan) {
errorReasons.push({
type: 'NO_PAYMENT_WITH_PRICED_PLAN',

View File

@@ -56,6 +56,7 @@ export default () => {
app.use('/organization', Container.get(Organization).router());
app.use('/ping', Container.get(Ping).router());
app.use('/setup', Container.get(Setup).router());
// - Dashboard routes.
// ---------------------------
const dashboard = Router();