diff --git a/client/src/components/Dashboard/Dashboard.js b/client/src/components/Dashboard/Dashboard.js index 996587acc..7be031c16 100644 --- a/client/src/components/Dashboard/Dashboard.js +++ b/client/src/components/Dashboard/Dashboard.js @@ -15,8 +15,10 @@ export default function Dashboard() {
- - + + + + @@ -32,4 +34,4 @@ export default function Dashboard() {
); -} \ No newline at end of file +} diff --git a/client/src/config/sidebarMenu.js b/client/src/config/sidebarMenu.js index 8945fa9dd..f04efbf8d 100644 --- a/client/src/config/sidebarMenu.js +++ b/client/src/config/sidebarMenu.js @@ -194,6 +194,10 @@ export default [ text: , href: '/preferences', }, + { + text: , + href: '/billing', + }, { text: , href: '/auditing/list', diff --git a/client/src/containers/Subscriptions/BillingForm.js b/client/src/containers/Subscriptions/BillingForm.js new file mode 100644 index 000000000..33b1fafb8 --- /dev/null +++ b/client/src/containers/Subscriptions/BillingForm.js @@ -0,0 +1,94 @@ +import React, { useEffect, useMemo, useState } from 'react'; +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'; +import { compose } from 'utils'; + +function BillingForm({ + // #withDashboardActions + changePageTitle, + + //#withBillingActions + requestSubmitBilling, +}) { + // const defaultPlan = useMemo(() => ({ + // plan_slug: [ + // { id: 0, name: 'Basic', value: 'basic' }, + // { id: 0, name: 'Pro', value: 'pro' }, + // ], + // })); + + const { formatMessage } = useIntl(); + + useEffect(() => { + changePageTitle(formatMessage({ id: 'billing' })); + }, [changePageTitle, formatMessage]); + + const validationSchema = Yup.object().shape({ + plan_slug: Yup.string() + .required() + .label(formatMessage({ id: 'plan_slug' })), + license_code: Yup.string().trim(), + }); + + const initialValues = useMemo( + () => ({ + plan_slug: 'basic', + license_code: '', + }), + [], + ); + + const formik = useFormik({ + enableReinitialize: true, + validationSchema: validationSchema, + 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) => { + setSubmitting(false); + }); + }, + }); + console.log(formik.values, 'formik'); + return ( +
+
+ +
+ +
+ +
+ ); +} + +export default compose(withDashboardActions, withBillingActions)(BillingForm); diff --git a/client/src/containers/Subscriptions/BillingTab.js b/client/src/containers/Subscriptions/BillingTab.js new file mode 100644 index 000000000..df46a8977 --- /dev/null +++ b/client/src/containers/Subscriptions/BillingTab.js @@ -0,0 +1,164 @@ +import React, { + useState, + useMemo, + useCallback, + useEffect, + useRef, +} from 'react'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { PaymentMethodTabs } from './SubscriptionTabs'; + +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 ( +
+
+

+ +

+

+ +

+ +
+ +
+

+ +

+

+ +

+ +
+
+

+ +

+

+ +

+ +
+
+ ); +} + +export default BillingTab; diff --git a/client/src/containers/Subscriptions/LicenseTab.js b/client/src/containers/Subscriptions/LicenseTab.js new file mode 100644 index 000000000..e485db707 --- /dev/null +++ b/client/src/containers/Subscriptions/LicenseTab.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import { InputGroup, FormGroup, Intent } from '@blueprintjs/core'; +import ErrorMessage from 'components/ErrorMessage'; + +function LicenseTab({ + formik: { errors, touched, setFieldValue, getFieldProps }, +}) { + return ( +
+

+ +

+

+ +

+ + } + intent={errors.license_code && touched.license_code && Intent.DANGER} + helperText={ + + } + className={'form-group-license_code'} + > + + +
+ ); +} + +export default LicenseTab; diff --git a/client/src/containers/Subscriptions/SubscriptionTabs.js b/client/src/containers/Subscriptions/SubscriptionTabs.js new file mode 100644 index 000000000..7113fbc6b --- /dev/null +++ b/client/src/containers/Subscriptions/SubscriptionTabs.js @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import { Tabs, Tab } from '@blueprintjs/core'; +import { FormattedMessage as T, useIntl } from 'react-intl'; +import BillingTab from './BillingTab'; +import LicenseTab from './LicenseTab'; + +export const MeteredBillingTabs = ({ formik }) => { + const [animate, setAnimate] = useState(true); + const { formatMessage } = useIntl(); + + return ( +
+ + } + /> + + +
+ ); +}; + +export const PaymentMethodTabs = ({ formik }) => { + const [animate, setAnimate] = useState(true); + const { formatMessage } = useIntl(); + + return ( +
+ + } + /> + + + +
+ ); +}; diff --git a/client/src/containers/Subscriptions/withBillingActions.js b/client/src/containers/Subscriptions/withBillingActions.js new file mode 100644 index 000000000..a4f8e5586 --- /dev/null +++ b/client/src/containers/Subscriptions/withBillingActions.js @@ -0,0 +1,8 @@ +import { connect } from 'react-redux'; +import { submitBilling } from 'store/billing/Billing.action'; + +export const mapDispatchToProps = (dispatch) => ({ + requestSubmitBilling: (form) => dispatch(submitBilling({ form })), +}); + +export default connect(null, mapDispatchToProps); diff --git a/client/src/lang/en/index.js b/client/src/lang/en/index.js index df8da8da8..58dd4d3ca 100644 --- a/client/src/lang/en/index.js +++ b/client/src/lang/en/index.js @@ -670,4 +670,30 @@ export default { 'The payment receive has been successfully created.', select_invoice: 'Select Invoice', payment_mades: 'Payment Mades', + + subscription: 'Subscription', + plan_slug: 'Plan slug', + billing: 'Billing', + the_billing_has_been_successfully_created: + 'The billing has been successfully created.', + a_select_a_plan: 'A. Select a plan', + b_choose_your_billing: 'B. Choose your billing', + c_payment_methods: 'C. Payment methods', + usage: 'Usage', + basic: 'Basic', + license: 'License', + credit_card: 'Credit Card', + paypal: 'Paypal', + pro: 'PRO', + monthly: 'Monthly', + yearly: 'Yearly', + license_code: 'License code', + year: 'Year', + please_enter_your_preferred_payment_method: + 'Please enter your preferred payment method below. You can use a credit / debit card or prepay through PayPal. ', + cards_will_be_charged: + 'Cards will be charged either at the end of the month or whenever your balance exceeds the usage threshold.All major credit / debit cards accepted.', + license_number: 'License number', + subscribe: 'Subscribe', + year_per: 'year', }; diff --git a/client/src/routes/dashboard.js b/client/src/routes/dashboard.js index 90eacfa50..303596f3f 100644 --- a/client/src/routes/dashboard.js +++ b/client/src/routes/dashboard.js @@ -322,4 +322,16 @@ export default [ }), breadcrumb: 'Receipt List', }, + +//Subscriptions + + { + path: `/billing`, + component: LazyLoader({ + loader: () => import('containers/Subscriptions/BillingForm'), + }), + breadcrumb: 'New Billing', + }, + + ]; diff --git a/client/src/store/billing/Billing.action.js b/client/src/store/billing/Billing.action.js new file mode 100644 index 000000000..e1ca8945f --- /dev/null +++ b/client/src/store/billing/Billing.action.js @@ -0,0 +1,18 @@ +import ApiService from 'services/ApiService'; +import t from 'store/types'; + +export const submitBilling = ({ form }) => { + return (dispatch) => + new Promise((resolve, reject) => { + ApiService.post('payment', form) + .then((response) => { + resolve(response); + }) + .catch((error) => { + const { response } = error; + const { data } = response; + + reject(data?.errors); + }); + }); +}; diff --git a/client/src/store/billing/Billing.type.js b/client/src/store/billing/Billing.type.js new file mode 100644 index 000000000..e69de29bb diff --git a/client/src/store/types.js b/client/src/store/types.js index 182dd8562..4a5b44cc8 100644 --- a/client/src/store/types.js +++ b/client/src/store/types.js @@ -23,6 +23,7 @@ import receipts from './receipt/receipt.type'; import bills from './Bills/bills.type'; import paymentReceives from './PaymentReceive/paymentReceive.type'; import vendors from './vendors/vendors.types'; +import billing from './billing/Billing.type'; export default { ...authentication, @@ -50,4 +51,5 @@ export default { ...bills, ...paymentReceives, ...vendors, + ...billing }; diff --git a/client/src/style/App.scss b/client/src/style/App.scss index b01485625..4fd5a4ea6 100644 --- a/client/src/style/App.scss +++ b/client/src/style/App.scss @@ -24,6 +24,9 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, @import '@blueprintjs/core/src/blueprint.scss'; @import '@blueprintjs/datetime/src/blueprint-datetime.scss'; +// Bootstrap +// @import '~bootstrap/scss/bootstrap'; + @import 'basscss'; @import 'functions'; @@ -62,7 +65,7 @@ $pt-font-family: Noto Sans, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, @import 'pages/estimates'; @import 'pages/receipts'; @import 'pages/invoices'; - +@import 'pages/billing.scss'; // Views @import 'views/filter-dropdown'; diff --git a/client/src/style/pages/billing.scss b/client/src/style/pages/billing.scss new file mode 100644 index 000000000..4ca989d44 --- /dev/null +++ b/client/src/style/pages/billing.scss @@ -0,0 +1,161 @@ +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +.billing-form { + padding: 25px 45px; + width: 800px; + margin: 0 auto; + + &__plan-container { + display: flex; + flex-flow: row wrap; + margin-bottom: 30px; + + .plan-wrapper { + display: flex; + flex-direction: column; + width: 215px; + height: 267px; + border-radius: 5px; + padding: 15px; + border: 1px solid #dcdcdc; + background: #fcfdff; + text-decoration: none; + color: #000; + cursor: pointer; + + &.plan-wrapper:not(:first-child) { + margin-left: 20px; + } + + .plan-header { + display: flex; + justify-content: flex-start; + margin-bottom: 10px; + } + .plan-name { + background: #3657ff; + border-radius: 3px; + padding: 1px 8px 1px 8px; + font-size: 13px; + color: #fff; + margin-bottom: 15px; + } + .plan-description { + font-size: 14px; + font-weight: 400; + line-height: 2em; + + &.plan-description ul { + list-style: none; + } + } + .plan-price { + margin-top: auto; + + .amount { + font-weight: 500; + } + + .period { + font-weight: 400; + color: #666; + &.period::before { + content: '/'; + display: inline-block; + margin: 0 2px; + } + } + } + } + a.plan-selected { + border: 1px solid #0069ff; + background-color: #fcfdff; + } + } + .payment-method-continer { + margin-bottom: 30px; + .period-container { + display: inline-flex; + background-color: #fcfdff; + justify-content: space-between; + align-items: center; + width: 215px; + height: 36px; + border-radius: 5px; + padding: 8px 10px; + color: #000; + border: 1px solid #dcdcdc; + cursor: pointer; + text-decoration: none; + &.period-container:not(:first-child) { + margin-left: 20px; + } + .period::before { + content: '/'; + display: inline-block; + margin: 0 2px; + } + .bg-period { + font-size: 14px; + font-weight: 500; + } + } + a.billing-selected { + border: 1px solid #0069ff; + background-color: #fcfdff; + } + } + + .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; + } + } + .bp3-form-content { + .bp3-input-group { + display: block; + position: relative; + } + .bp3-input { + position: relative; + width: 59%; + height: 41px; + } + } + h4 { + font-size: 18px; + color: #444444; + } + p { + font-size: 14px; + } + } + + .bp3-tab-list { + border-bottom: 2px solid #f1f1f1; + width: 95%; + } + .subscribe-button { + .bp3-button { + background-color: #0063ff; + min-height: 41px; + width: 240px; + // width: 25%; + } + } +} diff --git a/client/src/style/pages/preferences.scss b/client/src/style/pages/preferences.scss index 5d9b25712..95aeefa1b 100644 --- a/client/src/style/pages/preferences.scss +++ b/client/src/style/pages/preferences.scss @@ -1,6 +1,8 @@ .dashboard-content--preferences { - margin-left: 430px; - height: 700px; + // margin-left: 430px; + margin-left: 410px; + width: 100%; + // height: max-content; position: relative; } @@ -58,7 +60,9 @@ .preferences__sidebar { background: #fdfdfd; position: fixed; - left: 220px; + // left: 220px; + left: 200px; + top: 1px; min-width: 210px; max-width: 210px; height: 100%; @@ -104,10 +108,9 @@ // Preference //--------------------------------- .preferences__inside-content--general { - .bp3-form-group { margin: 25px 20px 20px; - + .bp3-label { min-width: 180px; } @@ -117,8 +120,7 @@ } .form-group--org-name, - .form-group--org-industry{ - + .form-group--org-industry { .bp3-form-content { position: relative; width: 70%;