├── .prettierrc.json
├── src
├── common
│ ├── index.js
│ ├── crossStorage.js
│ └── PrimaryLayout.js
├── layouts
│ ├── index.js
│ ├── MainToolbar.js
│ ├── AddDialog.js
│ ├── MainDrawer.js
│ └── MainLayout.js
├── utils.js
├── setupTests.js
├── error
│ ├── configuration.js
│ ├── Error404.js
│ └── Error500.js
├── workspace
│ ├── webhook
│ │ └── ViewWebhooks.js
│ ├── plan
│ │ ├── EditPlan.js
│ │ ├── ViewPlan.js
│ │ ├── PlanCard.js
│ │ └── PlanFormDrawer.js
│ ├── common
│ │ ├── FormsyTextField.js
│ │ ├── NoRecords.js
│ │ ├── Lookup.js
│ │ ├── CountrySelect.js
│ │ ├── WorkspaceTableHead.js
│ │ ├── WorkspaceToolbar.js
│ │ ├── WorkspaceTable.js
│ │ └── WorkspaceFilter.js
│ ├── transaction
│ │ ├── EditTransaction.js
│ │ ├── ViewTransaction.js
│ │ ├── TransactionCard.js
│ │ └── TransactionFormDrawer.js
│ ├── account
│ │ ├── EditAccount.js
│ │ ├── AccountCard.js
│ │ ├── ViewAccount.js
│ │ └── AccountFormDrawer.js
│ ├── invoice
│ │ ├── EditInvoice.js
│ │ ├── InvoiceCardTable.js
│ │ ├── InvoiceFormDrawer.js
│ │ └── ViewInvoice.js
│ ├── analytics
│ │ ├── PlanCharts.js
│ │ ├── SubscriberCharts.js
│ │ ├── RevenueCharts.js
│ │ ├── Summary.js
│ │ ├── SubscriptionsSummary.js
│ │ ├── RevenueSummary.js
│ │ ├── PlanSummary.js
│ │ ├── LineGraph.js
│ │ ├── BarGraph.js
│ │ └── Analytics.js
│ ├── configuration.js
│ ├── preferences
│ │ └── PreferenceForms.js
│ ├── subscription
│ │ ├── ViewSubscription.js
│ │ └── SubscriptionFormDrawer.js
│ └── api-key
│ │ └── APIKeyFormDrawer.js
├── redux
│ ├── store.js
│ ├── actionTypes.js
│ └── reducers.js
├── routes.js
├── index.js
├── serviceWorker.js
└── server
│ └── api.js
├── public
├── robots.txt
├── assets
│ └── images
│ │ ├── 404.png
│ │ ├── 500.png
│ │ ├── cycle.png
│ │ ├── favicon.png
│ │ ├── hubble.png
│ │ ├── coming-soon.png
│ │ └── maintenance.png
├── manifest.json
└── 404.html
├── .env.sample
├── .prettierignore
├── .gitignore
├── docs
└── contribute
│ ├── new-dialog.md
│ ├── sso.md
│ └── adding-list-entity-page.md
├── package.json
└── README.md
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 4
3 | }
4 |
--------------------------------------------------------------------------------
/src/common/index.js:
--------------------------------------------------------------------------------
1 | export { default as PrimaryLayout } from "./PrimaryLayout";
2 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/assets/images/404.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itspaywall/merchant-console/HEAD/public/assets/images/404.png
--------------------------------------------------------------------------------
/public/assets/images/500.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itspaywall/merchant-console/HEAD/public/assets/images/500.png
--------------------------------------------------------------------------------
/public/assets/images/cycle.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itspaywall/merchant-console/HEAD/public/assets/images/cycle.png
--------------------------------------------------------------------------------
/public/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itspaywall/merchant-console/HEAD/public/assets/images/favicon.png
--------------------------------------------------------------------------------
/public/assets/images/hubble.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itspaywall/merchant-console/HEAD/public/assets/images/hubble.png
--------------------------------------------------------------------------------
/public/assets/images/coming-soon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itspaywall/merchant-console/HEAD/public/assets/images/coming-soon.png
--------------------------------------------------------------------------------
/public/assets/images/maintenance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/itspaywall/merchant-console/HEAD/public/assets/images/maintenance.png
--------------------------------------------------------------------------------
/src/layouts/index.js:
--------------------------------------------------------------------------------
1 | import MainLayout from "./MainLayout";
2 |
3 | const layouts = {
4 | main: MainLayout,
5 | };
6 |
7 | export default layouts;
8 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export function toDateString(date) {
2 | const options = { year: "numeric", month: "long", day: "numeric" };
3 | return date.toLocaleDateString(undefined, options);
4 | }
5 |
--------------------------------------------------------------------------------
/.env.sample:
--------------------------------------------------------------------------------
1 | REACT_APP_API_URL=https://api.hubblesuite.com
2 | REACT_APP_WEBSITE_URL=https://hubblesuite.com
3 | REACT_APP_CONSOLE_URL=https://subscriptions.hubblesuite.com
4 | REACT_APP_DOCS_URL=https://docs.hubblesuite.com
5 | PORT=80
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import "@testing-library/jest-dom/extend-expect";
6 |
--------------------------------------------------------------------------------
/src/error/configuration.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const errorConfiguration = [
4 | {
5 | path: "/error/404",
6 | component: React.lazy(() => import("./Error404")),
7 | },
8 | {
9 | path: "/error/500",
10 | component: React.lazy(() => import("./Error500")),
11 | },
12 | ];
13 |
14 | export default errorConfiguration;
15 |
--------------------------------------------------------------------------------
/src/common/crossStorage.js:
--------------------------------------------------------------------------------
1 | import crossStorage from "cross-storage";
2 |
3 | const newCrossStorage = async () => {
4 | const result = new crossStorage.CrossStorageClient(
5 | `${process.env.REACT_APP_WEBSITE_URL}/hub.html`
6 | );
7 | await result.onConnect();
8 | return result;
9 | };
10 |
11 | export default {
12 | connection: newCrossStorage(),
13 | };
14 |
--------------------------------------------------------------------------------
/src/workspace/webhook/ViewWebhooks.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import NoRecords from "../common/NoRecords";
3 |
4 | export default function ViewWebhooks(props) {
5 | return (
6 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.development
17 | .env.production
18 | .env.local
19 | .env.development.local
20 | .env.test.local
21 | .env.production.local
22 |
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
--------------------------------------------------------------------------------
/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import thunk from "redux-thunk";
2 | import { composeWithDevTools } from "redux-devtools-extension";
3 | import { createStore, applyMiddleware, compose } from "redux";
4 | import rootReducer from "./reducers";
5 |
6 | let store;
7 | if (process.env.NODE_ENV === "development") {
8 | store = createStore(
9 | rootReducer,
10 | compose(applyMiddleware(thunk), composeWithDevTools())
11 | );
12 | } else {
13 | store = createStore(rootReducer, compose(applyMiddleware(thunk)));
14 | }
15 |
16 | export default store;
17 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Redirect } from "react-router-dom";
3 |
4 | import workspaceConfiguration from "./workspace/configuration";
5 | import errorConfiguration from "./error/configuration";
6 |
7 | const routes = [
8 | ...workspaceConfiguration,
9 | ...errorConfiguration,
10 | {
11 | path: "/",
12 | exact: true,
13 | component: () => ,
14 | },
15 | {
16 | component: () => ,
17 | },
18 | ];
19 |
20 | export default routes;
21 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "Hubble",
3 | "name": "Hubble",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/src/common/PrimaryLayout.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withStyles } from "@material-ui/core/styles";
3 | import { withRouter } from "react-router-dom";
4 | import layouts from "../layouts";
5 |
6 | const styles = (theme) => ({
7 | root: {
8 | backgroundColor: theme.palette.background.default,
9 | color: theme.palette.text.primary,
10 | },
11 | });
12 |
13 | function PrimaryLayout(props) {
14 | const { classes } = props;
15 |
16 | // const Layout = layouts[settings.layout.name];
17 | const Layout = layouts["main"];
18 | return ;
19 | }
20 |
21 | export default withStyles(styles)(withRouter(PrimaryLayout));
22 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import { Provider } from "react-redux";
4 |
5 | import App from "./App";
6 | import * as serviceWorker from "./serviceWorker";
7 | import store from "./redux/store";
8 |
9 | ReactDOM.render(
10 |
11 |
12 |
13 |
14 | ,
15 | document.getElementById("root")
16 | );
17 |
18 | // If you want your app to work offline and load faster, you can change
19 | // unregister() to register() below. Note this comes with some pitfalls.
20 | // Learn more about service workers: https://bit.ly/CRA-PWA
21 | serviceWorker.unregister();
22 |
--------------------------------------------------------------------------------
/docs/contribute/new-dialog.md:
--------------------------------------------------------------------------------
1 | ## Steps to create New Dialog
2 |
3 | 1. Create `src/workspace/subscription/NewSubscription.js` with the `NewSubscription` component. See `NewAccount` for reference.
4 | 2. Next open `src/layouts/MainLayout.js`, where you will see how we are opening `NewAccount` dialog. Add another line that opens `NewSubscription` dialog similarly. Basically, when the user clicks "New Subscription" in Quick Add dialog, the `newSubscription()` action (defined in `src/actions.js`) will be invoked which dispatches the `NEW_SUBSCRIPTION` action. The `dialogReducer` (defined in `src/reducers.js`) updates the store by setting `store.openDialog` to `NEW_SUBSCRIPTION`. So, in `MainLayout.js`, you can write `{ (openDialog == 'NEW_SUBSCRIPTION) && }`.
5 |
--------------------------------------------------------------------------------
/src/workspace/plan/EditPlan.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 |
4 | import * as actions from "../../redux/actions";
5 | import PlanFormDrawer from "./PlanFormDrawer";
6 |
7 | function EditPlan(props) {
8 | const { plan, savePlan } = props;
9 | return (
10 |
17 | );
18 | }
19 |
20 | function mapStateToProps(state) {
21 | return {
22 | plan: state.plan,
23 | };
24 | }
25 |
26 | const mapDispatchToProps = {
27 | savePlan: actions.savePlan,
28 | };
29 |
30 | export default connect(mapStateToProps, mapDispatchToProps)(EditPlan);
31 |
--------------------------------------------------------------------------------
/src/workspace/common/FormsyTextField.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { TextField } from "@material-ui/core";
3 | import { withFormsy } from "formsy-react";
4 |
5 | function FormsyTextField(props) {
6 | const errorMessage = props.errorMessage;
7 | const value = props.value || "";
8 |
9 | const changeValue = (event) => {
10 | props.setValue(event.currentTarget.value);
11 | if (props.onChange) {
12 | props.onChange(event);
13 | }
14 | };
15 |
16 | return (
17 |
24 | );
25 | }
26 |
27 | export default withFormsy(FormsyTextField);
28 |
--------------------------------------------------------------------------------
/src/workspace/transaction/EditTransaction.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 |
4 | import * as actions from "../../redux/actions";
5 | import TransactionFormDrawer from "./TransactionFormDrawer";
6 |
7 | function EditTransaction(props) {
8 | const { transaction, saveTransaction } = props;
9 | return (
10 |
17 | );
18 | }
19 |
20 | function mapStateToProps(state) {
21 | return {
22 | transaction: state.transaction,
23 | };
24 | }
25 |
26 | const mapDispatchToProps = {
27 | saveTransaction: actions.saveTransaction,
28 | };
29 |
30 | export default connect(mapStateToProps, mapDispatchToProps)(EditTransaction);
31 |
--------------------------------------------------------------------------------
/src/workspace/account/EditAccount.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 |
4 | import * as actions from "../../redux/actions";
5 | import AccountFormDrawer from "./AccountFormDrawer";
6 |
7 | function EditAccount(props) {
8 | const { account, saveAccount } = props;
9 | const handleSave = (values) => {
10 | saveAccount({
11 | ...account,
12 | ...values,
13 | });
14 | };
15 | return (
16 |
23 | );
24 | }
25 |
26 | function mapStateToProps(state) {
27 | return {
28 | account: state.account,
29 | };
30 | }
31 |
32 | const mapDispatchToProps = {
33 | saveAccount: actions.saveAccount,
34 | };
35 |
36 | export default connect(mapStateToProps, mapDispatchToProps)(EditAccount);
37 |
--------------------------------------------------------------------------------
/src/workspace/invoice/EditInvoice.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { connect } from "react-redux";
3 |
4 | import * as actions from "../../redux/actions";
5 | import InvoiceFormDrawer from "./InvoiceFormDrawer";
6 |
7 | function EditInvoice(props) {
8 | const { invoice, saveInvoice } = props;
9 | const handleSave = (values) => {
10 | saveInvoice({
11 | ...invoice,
12 | ...values,
13 | });
14 | };
15 | return (
16 |
23 | );
24 | }
25 |
26 | function mapStateToProps(state) {
27 | return {
28 | invoice: state.invoice,
29 | };
30 | }
31 |
32 | const mapDispatchToProps = {
33 | saveInvoice: actions.saveInvoice,
34 | };
35 |
36 | export default connect(mapStateToProps, mapDispatchToProps)(EditInvoice);
37 |
--------------------------------------------------------------------------------
/docs/contribute/sso.md:
--------------------------------------------------------------------------------
1 | # SSO
2 |
3 | We use local storage to store the user details returned from the server after login and registration.
4 | An important piece of data is the JWT access token, which is used to make all the REST API calls.
5 | The problem is, local storage is accessible only within (protocol, domain, port) tuple. For example,
6 | if you create something in `http://localhost:3000`, you cannot access it in `http://localhost:3001`.
7 | Even though the protocol and the domain are the same, according to the same-origin policy, they are
8 | different.
9 |
10 | Since we implement SSO, that is, the user signs in at `hubblesuite.com` and can access the subdomains
11 | without having to login again, we need a way to access the JWT token from the subdomains. That is
12 | where cross storage library comes into picture. It uses an iframe to implement something the library
13 | calls a hub. In simple terms, a hub is _like_ a server running on the client side that helps us retrieve
14 | and update items in the local storage from different subdomains. The implementation detail of the library
15 | is a bit more complicated.
16 |
--------------------------------------------------------------------------------
/src/workspace/common/NoRecords.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Typography from "@material-ui/core/Typography";
3 | import Button from "@material-ui/core/Button";
4 | import { makeStyles } from "@material-ui/core/styles";
5 |
6 | const useStyles = makeStyles((theme) => ({
7 | container: {
8 | display: "flex",
9 | alignItems: "center",
10 | justifyContent: "center",
11 | flexDirection: "column",
12 | },
13 | image: {
14 | width: "auto",
15 | height: 400,
16 | marginTop: 96,
17 | },
18 | message: {
19 | fontSize: 20,
20 | fontWeight: 400,
21 | color: theme.palette.text.secondary,
22 | },
23 | action: {
24 | marginTop: 16,
25 | },
26 | }));
27 |
28 | export default function NoRecords(props) {
29 | const classes = useStyles();
30 | const { message, actionText, actionHandler, action, image } = props;
31 |
32 | return (
33 |
34 |
35 |
{message}
36 | {action && (
37 |
44 | {actionText}
45 |
46 | )}
47 |
48 | );
49 | }
50 |
--------------------------------------------------------------------------------
/src/redux/actionTypes.js:
--------------------------------------------------------------------------------
1 | export const NEW_API_KEY = "NEW_API_KEY";
2 |
3 | export const NEW_ACCOUNT = "NEW_ACCOUNT";
4 | export const FETCH_ACCOUNT_COMPLETE = "FETCH_ACCOUNT_COMPLETE";
5 | export const FETCH_ACCOUNTS_COMPLETE = "FETCH_ACCOUNTS_COMPLETE";
6 | export const EDIT_ACCOUNT = "EDIT_ACCOUNT";
7 | export const CLEAR_ACCOUNT = "CLEAR_ACCOUNT";
8 |
9 | export const NEW_SUBSCRIPTION = "NEW_SUBSCRIPTION";
10 | export const FETCH_SUBSCRIPTION_COMPLETE = "FETCH_SUBSCRIPTION_COMPLETE";
11 | export const FETCH_SUBSCRIPTIONS_COMPLETE = "FETCH_SUBSCRIPTIONS_COMPLETE";
12 | export const EDIT_SUBSCRIPTION = "EDIT_SUBSCRIPTION";
13 | export const CLEAR_SUBSCRIPTION = "CLEAR_SUBSCRIPTION";
14 |
15 | export const NEW_INVOICE = "NEW_INVOICE";
16 | export const FETCH_INVOICE_COMPLETE = "FETCH_INVOICE_COMPLETE";
17 | export const FETCH_INVOICES_COMPLETE = "FETCH_INVOICES_COMPLETE";
18 | export const EDIT_INVOICE = "EDIT_INVOICE";
19 | export const CLEAR_INVOICE = "CLEAR_INVOICE";
20 |
21 | export const NEW_TRANSACTION = "NEW_TRANSACTION";
22 | export const FETCH_TRANSACTION_COMPLETE = "FETCH_TRANSACTION_COMPLETE";
23 | export const FETCH_TRANSACTIONS_COMPLETE = "FETCH_TRANSACTIONS_COMPLETE";
24 | export const EDIT_TRANSACTION = "EDIT_TRANSACTION";
25 | export const CLEAR_TRANSACTION = "CLEAR_TRANSACTION";
26 |
27 | export const NEW_PLAN = "NEW_PLAN";
28 | export const FETCH_PLAN_COMPLETE = "FETCH_PLAN_COMPLETE";
29 | export const FETCH_PLANS_COMPLETE = "FETCH_PLANS_COMPLETE";
30 | export const EDIT_PLAN = "EDIT_PLAN";
31 | export const CLEAR_PLAN = "CLEAR_PLAN";
32 |
33 | export const FETCH_ANALYTICS_COMPLETE = "FETCH_ANALYTICS_COMPLETE";
34 |
35 | export const CLOSE_DIALOG = "CLOSE_DIALOG";
36 | export const SHOW_NOTIFICATION = "SHOW_NOTIFICATION";
37 | export const CLOSE_NOTIFICATION = "CLOSE_NOTIFICATION";
38 |
39 | export const FETCH_USER_COMPLETE = "FETCH_USER_COMPLETE";
40 | export const FETCH_USER_FAILED = "FETCH_USER_FAILED";
41 |
42 | export const INTERNAL_REDIRECT = "INTERNAL_REDIRECT";
43 |
--------------------------------------------------------------------------------
/src/error/Error404.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withStyles, Grid, Typography } from "@material-ui/core";
3 | import { Link } from "react-router-dom";
4 |
5 | const styles = (theme) => ({
6 | root: {},
7 | image: {
8 | marginTop: 120,
9 | width: 600,
10 | height: "auto",
11 | display: "block",
12 | marginLeft: "auto",
13 | marginRight: "auto",
14 | },
15 | title: {
16 | marginTop: 16,
17 | textAlign: "center",
18 | },
19 | description: {
20 | marginTop: 16,
21 | textAlign: "center",
22 | },
23 | link: {
24 | marginTop: 16,
25 | textAlign: "center",
26 | display: "block",
27 | },
28 | });
29 |
30 | function Error404(props) {
31 | const { classes } = props;
32 | return (
33 |
34 |
35 |
40 |
45 | Looks like you are lost
46 |
47 |
48 |
53 | The page you are looking for may have been removed or moved
54 | to another location.
55 |
56 |
57 |
58 | Go back to analytics
59 |
60 |
61 |
62 | );
63 | }
64 |
65 | export default withStyles(styles)(Error404);
66 |
--------------------------------------------------------------------------------
/src/workspace/analytics/PlanCharts.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Card from "@material-ui/core/Card";
3 | import CardContent from "@material-ui/core/CardContent";
4 | import { makeStyles } from "@material-ui/core/styles";
5 | import BarGraph from "./BarGraph";
6 |
7 | const useStyles = makeStyles((theme) => ({
8 | root: {
9 | borderRadius: 0,
10 | minHeight: 700,
11 | maxHeight: 700,
12 | display: "flex",
13 | flexDirection: "column",
14 | justifyContent: "space-evenly",
15 | },
16 | }));
17 |
18 | function PlanCharts(props) {
19 | const classes = useStyles();
20 |
21 | const { planData, conversionData } = props;
22 |
23 | return (
24 |
25 |
26 |
44 |
52 |
53 |
54 | );
55 | }
56 |
57 | export default PlanCharts;
58 |
--------------------------------------------------------------------------------
/src/error/Error500.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { withStyles, Grid, Typography } from "@material-ui/core";
3 | import { Link } from "react-router-dom";
4 |
5 | const styles = (theme) => ({
6 | root: {},
7 | image: {
8 | marginTop: 120,
9 | width: 600,
10 | height: "auto",
11 | display: "block",
12 | marginLeft: "auto",
13 | marginRight: "auto",
14 | },
15 | title: {
16 | marginTop: 16,
17 | textAlign: "center",
18 | },
19 | description: {
20 | marginTop: 16,
21 | textAlign: "center",
22 | },
23 | link: {
24 | marginTop: 16,
25 | textAlign: "center",
26 | display: "block",
27 | },
28 | });
29 |
30 | function Error404(props) {
31 | const { classes } = props;
32 | return (
33 |
34 |
35 |
40 |
45 | Well, you broke the Internet!
46 |
47 |
48 |
53 | Just kidding! Looks like we have an internal issue, please
54 | try again in a couple of minutes.
55 |
56 |
57 |
58 | Go back to analytics
59 |
60 |
61 |
62 | );
63 | }
64 |
65 | export default withStyles(styles)(Error404);
66 |
--------------------------------------------------------------------------------
/src/workspace/analytics/SubscriberCharts.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Card from "@material-ui/core/Card";
3 | import CardContent from "@material-ui/core/CardContent";
4 | import { makeStyles } from "@material-ui/core/styles";
5 | import LineGraph from "./LineGraph";
6 |
7 | const useStyles = makeStyles((theme) => ({
8 | root: {
9 | borderRadius: 0,
10 | minHeight: 700,
11 | maxHeight: 700,
12 | display: "flex",
13 | flexDirection: "column",
14 | justifyContent: "space-evenly",
15 | },
16 | content: {
17 | minHeight: 600,
18 | maxHeight: 600,
19 | display: "flex",
20 | flexDirection: "column",
21 | justifyContent: "center",
22 | },
23 | style: {
24 | margin: 20,
25 | width: "auto",
26 | height: 240,
27 | },
28 | space: {
29 | margin: 4,
30 | },
31 | }));
32 |
33 | function SubscriberCharts(props) {
34 | const classes = useStyles();
35 |
36 | const { subscriberData, churnRateData } = props;
37 |
38 | return (
39 |
40 |
41 |
49 |
50 |
58 |
59 |
60 | );
61 | }
62 |
63 | export default SubscriberCharts;
64 |
--------------------------------------------------------------------------------
/src/workspace/analytics/RevenueCharts.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Card from "@material-ui/core/Card";
3 | import CardContent from "@material-ui/core/CardContent";
4 | import { makeStyles } from "@material-ui/core/styles";
5 | import BarGraph from "./BarGraph";
6 |
7 | const useStyles = makeStyles((theme) => ({
8 | root: {
9 | borderRadius: 0,
10 | minHeight: 700,
11 | maxHeight: 700,
12 | display: "flex",
13 | flexDirection: "column",
14 | justifyContent: "space-evenly",
15 | },
16 | }));
17 |
18 | function RevenueCharts(props) {
19 | const classes = useStyles();
20 |
21 | const { revenueData, transactionData } = props;
22 |
23 | return (
24 |
25 |
26 |
34 |
54 |
55 |
56 | );
57 | }
58 |
59 | export default RevenueCharts;
60 |
--------------------------------------------------------------------------------
/src/workspace/common/Lookup.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import React from "react";
4 | import TextField from "@material-ui/core/TextField";
5 | import Autocomplete from "@material-ui/lab/Autocomplete";
6 | import { makeStyles } from "@material-ui/core/styles";
7 | import { withFormsy } from "formsy-react";
8 |
9 | const useStyles = makeStyles({
10 | option: {},
11 | });
12 |
13 | function Lookup(props) {
14 | const {
15 | label,
16 | options,
17 | updateOptions,
18 | value,
19 | onChange,
20 | renderOption,
21 | renderOptionLabel,
22 | name,
23 | required,
24 | } = props;
25 | const classes = useStyles();
26 | const [inputValue, setInputValue] = React.useState("");
27 |
28 | return (
29 | (
39 |
51 | )}
52 | onInputChange={(event, inputValue, reason) => {
53 | if (reason === "input") {
54 | // Convert empty strings to null.
55 | updateOptions(inputValue ? inputValue : null);
56 | }
57 | setInputValue(inputValue);
58 | }}
59 | inputValue={inputValue}
60 | onChange={onChange}
61 | value={value}
62 | />
63 | );
64 | }
65 |
66 | export default withFormsy(Lookup);
67 |
--------------------------------------------------------------------------------
/src/workspace/analytics/Summary.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Typography from "@material-ui/core/Typography";
4 | import Up from "@material-ui/icons/ExpandLessSharp";
5 | import Down from "@material-ui/icons/ExpandMoreSharp";
6 |
7 | const useStyles = makeStyles((theme) => ({
8 | content: {
9 | paddingTop: 24,
10 | paddingBottom: 24,
11 | flex: "1 0 auto",
12 | },
13 | button: {
14 | padding: 15,
15 | marginLeft: "auto",
16 | },
17 | upIcon: {
18 | verticalAlign: "bottom",
19 | fontSize: 32,
20 | },
21 | up: {
22 | color: theme.palette.success.main,
23 | },
24 | downIcon: {
25 | verticalAlign: "bottom",
26 | fontSize: 32,
27 | },
28 | down: {
29 | color: theme.palette.error.main,
30 | },
31 | small: {
32 | fontSize: 14,
33 | },
34 | }));
35 |
36 | export default function Summary(props) {
37 | const classes = useStyles();
38 | return (
39 |
40 |
41 | {props.title}
42 |
43 |
48 | {props.period}
49 |
50 |
51 | {props.number}
52 |
53 |
54 | {props.delta === "positive" ? (
55 |
56 |
57 | {props.change}
58 |
59 | ) : (
60 |
61 |
62 | {props.change}
63 |
64 | )}
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hubble-console",
3 | "version": "0.1.0",
4 | "homepage": "https://subscriptions.hubblesuite.com/",
5 | "private": true,
6 | "dependencies": {
7 | "@date-io/date-fns": "1.x",
8 | "@material-ui/core": "^4.11.0",
9 | "@material-ui/icons": "^4.9.1",
10 | "@material-ui/lab": "^4.0.0-alpha.56",
11 | "@material-ui/pickers": "^3.2.10",
12 | "@testing-library/jest-dom": "^4.2.4",
13 | "@testing-library/react": "^9.3.2",
14 | "@testing-library/user-event": "^7.1.2",
15 | "axios": "^0.19.2",
16 | "cross-storage": "^1.0.0",
17 | "date-fns": "^2.14.0",
18 | "formsy-react": "^2.2.1",
19 | "prop-types": "^15.7.2",
20 | "query-string": "^6.13.1",
21 | "react": "^16.13.1",
22 | "react-dom": "^16.13.1",
23 | "react-redux": "^7.2.1",
24 | "react-router": "^5.2.0",
25 | "react-router-config": "^5.1.1",
26 | "react-router-dom": "^5.2.0",
27 | "react-scripts": "^3.4.3",
28 | "recharts": "^1.8.5",
29 | "redux": "^4.0.5",
30 | "redux-devtools-extension": "^2.13.8",
31 | "redux-thunk": "^2.3.0"
32 | },
33 | "scripts": {
34 | "start": "react-scripts start",
35 | "build": "react-scripts build",
36 | "test": "react-scripts test",
37 | "eject": "react-scripts eject"
38 | },
39 | "eslintConfig": {
40 | "extends": "react-app"
41 | },
42 | "browserslist": {
43 | "production": [
44 | ">0.2%",
45 | "not dead",
46 | "not op_mini all"
47 | ],
48 | "development": [
49 | "last 1 chrome version",
50 | "last 1 firefox version",
51 | "last 1 safari version"
52 | ]
53 | },
54 | "devDependencies": {
55 | "axios-mock-adapter": "^1.18.1",
56 | "faker": "^4.1.0",
57 | "husky": "^4.2.5",
58 | "prettier": "^2.0.5",
59 | "pretty-quick": "^2.0.1",
60 | "redux-mock-store": "^1.5.4"
61 | },
62 | "husky": {
63 | "hooks": {
64 | "pre-commit": "pretty-quick --staged"
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/workspace/common/CountrySelect.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-use-before-define */
2 |
3 | import React from "react";
4 | import TextField from "@material-ui/core/TextField";
5 | import Autocomplete from "@material-ui/lab/Autocomplete";
6 | import { makeStyles } from "@material-ui/core/styles";
7 | import { withFormsy } from "formsy-react";
8 | import { data as countries } from "../../common/countries";
9 |
10 | /* ISO 3166-1 alpha-2
11 | * ⚠️ No support for IE 11
12 | */
13 | function countryToFlag(isoCode) {
14 | return typeof String.fromCodePoint !== "undefined"
15 | ? isoCode
16 | .toUpperCase()
17 | .replace(/./g, (character) =>
18 | String.fromCodePoint(character.charCodeAt(0) + 127397)
19 | )
20 | : isoCode;
21 | }
22 |
23 | const useStyles = makeStyles({
24 | option: {
25 | fontSize: 16,
26 | "& > span": {
27 | marginRight: 8,
28 | fontSize: 18,
29 | },
30 | },
31 | });
32 |
33 | function CountrySelect(props) {
34 | const { fullWidth, label, required, onChange, value } = props;
35 | const classes = useStyles();
36 |
37 | return (
38 | option.label}
47 | renderOption={(option) => (
48 |
49 | {countryToFlag(option.code)}
50 | {option.label}
51 |
52 | )}
53 | renderInput={(params) => (
54 |
64 | )}
65 | onChange={onChange}
66 | value={value}
67 | />
68 | );
69 | }
70 |
71 | export default withFormsy(CountrySelect);
72 |
--------------------------------------------------------------------------------
/src/workspace/configuration.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const workspaceConfiguration = [
4 | // API Key
5 | {
6 | path: "/api-keys",
7 | component: React.lazy(() => import("./api-key/ViewAPIKeys")),
8 | },
9 |
10 | // Webhooks
11 | {
12 | path: "/webhooks",
13 | component: React.lazy(() => import("./webhook/ViewWebhooks")),
14 | },
15 |
16 | // Account
17 | {
18 | path: "/accounts/:identifier",
19 | component: React.lazy(() => import("./account/ViewAccount")),
20 | },
21 | {
22 | path: "/accounts",
23 | component: React.lazy(() => import("./account/ViewAccounts")),
24 | exact: true,
25 | },
26 |
27 | // Invoice
28 | {
29 | path: "/invoices/:identifier",
30 | component: React.lazy(() => import("./invoice/ViewInvoice")),
31 | },
32 | {
33 | path: "/invoices",
34 | component: React.lazy(() => import("./invoice/ViewInvoices")),
35 | exact: true,
36 | },
37 |
38 | // Subscription
39 | {
40 | path: "/subscriptions/:identifier",
41 | component: React.lazy(() => import("./subscription/ViewSubscription")),
42 | },
43 | {
44 | path: "/subscriptions",
45 | component: React.lazy(() => import("./subscription/ViewSubscriptions")),
46 | exact: true,
47 | },
48 |
49 | // Transaction
50 | {
51 | path: "/transactions/:identifier",
52 | component: React.lazy(() => import("./transaction/ViewTransaction")),
53 | },
54 | {
55 | path: "/transactions",
56 | component: React.lazy(() => import("./transaction/ViewTransactions")),
57 | exact: true,
58 | },
59 |
60 | // Plan
61 | {
62 | path: "/plans/:identifier",
63 | component: React.lazy(() => import("./plan/ViewPlan")),
64 | },
65 | {
66 | path: "/plans",
67 | component: React.lazy(() => import("./plan/ViewPlans")),
68 | exact: true,
69 | },
70 |
71 | // Analytics
72 | {
73 | path: "/analytics",
74 | component: React.lazy(() => import("./analytics/Analytics")),
75 | exact: true,
76 | },
77 |
78 | // Preferences
79 | {
80 | path: "/preferences",
81 | component: React.lazy(() => import("./preferences/Preferences")),
82 | exact: true,
83 | },
84 | ];
85 |
86 | export default workspaceConfiguration;
87 |
--------------------------------------------------------------------------------
/src/workspace/analytics/SubscriptionsSummary.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Card from "@material-ui/core/Card";
4 | import CardContent from "@material-ui/core/CardContent";
5 | import Summary from "./Summary";
6 | import Divider from "@material-ui/core/Divider";
7 |
8 | const useStyles = makeStyles((theme) => ({
9 | root: {
10 | padding: 20,
11 | borderRadius: 0,
12 | minHeight: 700,
13 | maxHeight: 700,
14 | display: "flex",
15 | flexDirection: "column",
16 | justifyContent: "space-evenly",
17 | },
18 | details: {
19 | display: "flex",
20 | flexDirection: "column",
21 | },
22 | button: {
23 | padding: 15,
24 | marginLeft: "auto",
25 | },
26 | upIcon: {
27 | verticalAlign: "bottom",
28 | fontSize: 32,
29 | },
30 | up: {
31 | color: theme.palette.success.main,
32 | },
33 | downIcon: {
34 | verticalAlign: "bottom",
35 | fontSize: 32,
36 | },
37 | down: {
38 | color: theme.palette.error.main,
39 | },
40 | small: {
41 | fontSize: 14,
42 | },
43 | }));
44 |
45 | export default function SubscriptionsSummary(props) {
46 | const classes = useStyles();
47 | const { data } = props;
48 | return (
49 |
50 |
51 |
58 |
59 |
66 |
67 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/src/workspace/preferences/PreferenceForms.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Card from "@material-ui/core/Card";
4 | import CardActions from "@material-ui/core/CardActions";
5 | import CardContent from "@material-ui/core/CardContent";
6 | import Button from "@material-ui/core/Button";
7 | import RecordForm, { extractValues } from "../common/RecordForm";
8 |
9 | const useStyles = makeStyles((theme) => ({
10 | root: {
11 | margin: 0,
12 | borderRadius: 0,
13 | width: "50%",
14 | },
15 | details: {
16 | padding: 24,
17 | },
18 | actions: {
19 | width: "100%",
20 | display: "flex",
21 | flexDirection: "row-reverse",
22 | },
23 | action: {
24 | width: 100,
25 | },
26 | actionIcon: {
27 | marginRight: 4,
28 | display: "inline-block",
29 | },
30 | }));
31 |
32 | export default function PreferenceForms(props) {
33 | const classes = useStyles();
34 | const { groups } = props;
35 | const [values, setValues] = React.useState(extractValues(groups));
36 | const [saveDisabled, setSaveDisabled] = React.useState(true);
37 |
38 | const handleValueChange = (field, value) => {
39 | const newValues = JSON.parse(JSON.stringify(values));
40 | newValues[field.identifier] = value;
41 |
42 | setValues(newValues);
43 | };
44 | const tabIndex = 0;
45 | const showMore = true;
46 |
47 | return (
48 |
49 |
50 |
51 |
59 |
60 |
61 |
68 | Save
69 |
70 |
71 |
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/src/workspace/analytics/RevenueSummary.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Card from "@material-ui/core/Card";
4 | import CardContent from "@material-ui/core/CardContent";
5 | import Summary from "./Summary";
6 | import Divider from "@material-ui/core/Divider";
7 |
8 | const useStyles = makeStyles((theme) => ({
9 | root: {
10 | padding: 20,
11 | borderRadius: 0,
12 | minHeight: 700,
13 | maxHeight: 700,
14 | display: "flex",
15 | flexDirection: "column",
16 | justifyContent: "space-evenly",
17 | },
18 | details: {
19 | display: "flex",
20 | flexDirection: "column",
21 | },
22 | button: {
23 | padding: 15,
24 | marginLeft: "auto",
25 | },
26 | upIcon: {
27 | verticalAlign: "bottom",
28 | fontSize: 32,
29 | },
30 | up: {
31 | color: theme.palette.success.main,
32 | },
33 | downIcon: {
34 | verticalAlign: "bottom",
35 | fontSize: 32,
36 | },
37 | down: {
38 | color: theme.palette.error.main,
39 | },
40 | small: {
41 | fontSize: 14,
42 | },
43 | }));
44 |
45 | export default function RevenueSummary(props) {
46 | const classes = useStyles();
47 | const { data } = props;
48 | return (
49 |
50 |
51 |
58 |
59 |
66 |
67 |
74 |
75 |
76 | );
77 | }
78 |
--------------------------------------------------------------------------------
/src/workspace/analytics/PlanSummary.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Card from "@material-ui/core/Card";
4 | import CardContent from "@material-ui/core/CardContent";
5 | import Summary from "./Summary";
6 | import Divider from "@material-ui/core/Divider";
7 |
8 | const useStyles = makeStyles((theme) => ({
9 | root: {
10 | padding: 20,
11 | borderRadius: 0,
12 | minHeight: 700,
13 | maxHeight: 700,
14 | display: "flex",
15 | flexDirection: "column",
16 | justifyContent: "space-evenly",
17 | },
18 | details: {
19 | display: "flex",
20 | flexDirection: "column",
21 | },
22 | button: {
23 | padding: 15,
24 | marginLeft: "auto",
25 | },
26 | upIcon: {
27 | verticalAlign: "bottom",
28 | fontSize: 32,
29 | },
30 | up: {
31 | color: theme.palette.success.main,
32 | },
33 | downIcon: {
34 | verticalAlign: "bottom",
35 | fontSize: 32,
36 | },
37 | down: {
38 | color: theme.palette.error.main,
39 | },
40 | small: {
41 | fontSize: 14,
42 | },
43 | }));
44 |
45 | export default function PlanSummary(props) {
46 | const classes = useStyles();
47 | const { data } = props;
48 |
49 | return (
50 |
51 |
52 |
59 |
60 |
67 |
68 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/public/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Single Page Apps for GitHub Pages
6 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/src/workspace/invoice/InvoiceCardTable.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Table from "@material-ui/core/Table";
4 | import TableBody from "@material-ui/core/TableBody";
5 | import TableCell from "@material-ui/core/TableCell";
6 | import TableContainer from "@material-ui/core/TableContainer";
7 | import TableHead from "@material-ui/core/TableHead";
8 | import TableRow from "@material-ui/core/TableRow";
9 | import { grey } from "@material-ui/core/colors";
10 | import { toDateString } from "../../utils";
11 |
12 | const useStyles = makeStyles({
13 | table: {
14 | width: "100%",
15 | borderLeftWidth: 2,
16 | borderLeftColor: grey[200],
17 | borderLeftStyle: "solid",
18 | borderRightWidth: 2,
19 | borderRightColor: grey[200],
20 | borderRightStyle: "solid",
21 | },
22 | head: {
23 | backgroundColor: grey[200],
24 | },
25 | dateCell: {
26 | width: 300,
27 | },
28 | });
29 |
30 | export default function InvoiceCardTable(props) {
31 | const { rows } = props;
32 | const classes = useStyles();
33 |
34 | return (
35 |
36 |
37 |
38 |
39 | Period
40 | Description
41 | Quantity
42 | Price
43 | Subtotal
44 |
45 |
46 |
47 | {rows.map((row) => (
48 |
49 |
50 | {toDateString(row.startedAt) +
51 | "—" +
52 | toDateString(row.endedAt)}
53 |
54 | {row.description}
55 | {row.quantity}
56 | {row.price} INR
57 |
58 | {row.subtotal} INR
59 |
60 |
61 | ))}
62 |
63 |
64 |
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/src/workspace/invoice/InvoiceFormDrawer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { extractValues } from "../common/RecordForm";
5 | import FormDrawer from "../common/FormDrawer";
6 |
7 | // text, large_text, number, date_picker, date_range_picker, switch, phone_number, email_address
8 | // multiple_options (multiselect), single_option (drop down)
9 | // lookup - organization, user, invoice
10 |
11 | // Only top level children can have quickAdd. Groups cannot have required, unique, multipleValues, mininmumValues, maximumValues.
12 | // Groups can have readOnly, hidden, tooltip
13 | // The same person can work in multiple organizations. But such cases are rare. Therefore, the system should be kept
14 | // simple and not accomodate such cases. given, there are other work arounds.
15 |
16 | // The user name should be unique across your organization.
17 | const groups = [
18 | {
19 | label: "Basic",
20 | children: [
21 | {
22 | label: "Notes",
23 | identifier: "notes",
24 | type: "large_text",
25 | rows: 4,
26 | required: false,
27 | readOnly: false,
28 | quickAdd: true,
29 | unique: false,
30 | hidden: false,
31 | tooltip: "The notes for the invoice.",
32 | multipleValues: false,
33 | defaultValue: "",
34 | },
35 | {
36 | label: "Terms and Conditions",
37 | identifier: "termsAndConditions",
38 | rows: 4,
39 | type: "large_text",
40 | required: false,
41 | readOnly: false,
42 | quickAdd: true,
43 | unique: false,
44 | hidden: false,
45 | tooltip: "The terms and conditions applied to the invoice.",
46 | multipleValues: false,
47 | defaultValue: "",
48 | },
49 | ],
50 | },
51 | ];
52 |
53 | function InvoiceFormDrawer(props) {
54 | const { title, onSave, showMore, open } = props;
55 |
56 | const values = props.invoice || extractValues(groups);
57 | return (
58 |
66 | );
67 | }
68 |
69 | InvoiceFormDrawer.propTypes = {
70 | title: PropTypes.string.isRequired,
71 | showMore: PropTypes.bool,
72 | invoice: PropTypes.object,
73 | onSave: PropTypes.func.isRequired,
74 | };
75 |
76 | InvoiceFormDrawer.defaultProps = {
77 | showMore: false,
78 | invoice: null,
79 | onCancel: null,
80 | };
81 |
82 | export default InvoiceFormDrawer;
83 |
--------------------------------------------------------------------------------
/docs/contribute/adding-list-entity-page.md:
--------------------------------------------------------------------------------
1 | Create a function called `createXYZ()` in `actions.js`, where `XYZ` represents
2 | the entity for which you are creating the page. This function is responsible for
3 | creating a single instance. For the sake of this documentation, let's assume
4 | that you are creating a transaction. Therefore, the function will be named `createTransaction()`.
5 | At this point, you should know the attributes in the transaction object.
6 | So use `Faker.js` to fill the object with random data.
7 |
8 | Next create a constant called `DEFAULT_XYZ`. In our example, this would be `DEFAULT_TRANSACTIONS`
9 | that tells how many fake entries will be generated. Use a for loop and create that many objects
10 | by repeatedly invoking the function you just created. Since transaction depends on another object,
11 | you need to ensure that your for loop comes after the for loop that creates subscriptions.
12 | You can now modify your `createTransaction()` function to select a random subscription like this:
13 |
14 | ```
15 | const transaction = {
16 | ...
17 | subscription: faker.random.arrayElement(subscription),
18 | ...
19 | }
20 | ```
21 |
22 | Now that you have created the fake data, you can start working on the mock backend.
23 |
24 | There are generally five methods you need to implement:
25 |
26 | - GET all the transactions
27 | - GET a specific transaction
28 | - POST a transaction
29 | - PUT a transaction
30 | - DELETE a transaction
31 |
32 | Here, the first two and the last are obvious. The POST and PUT methods are used to create and edit,
33 | respectively. The implementation for getting all the transactions is pretty straight forward.
34 | You simply return all the items from the fake database table, which is an array in the mock backend.
35 | As for the other methods, you need to get the identifier for the transaction you want work with from
36 | the resource URL. This is where the regex pattern comes in. You can copy the regex from existing
37 | methods. You need to modify the regex as needed. For the transaction entity, you just need to add
38 | "transactions" to the URL by removing the old entity type. It is a good idea to create a constant
39 | to represent the regex because it will be reused inside the method when extracting the value.
40 |
41 | The general URL form is:
42 |
43 | ```
44 | /api/v1/entity/identifier
45 | ```
46 |
47 | Axios Mock Adapter does not provide an out of the box solution to retrieve the identifier. So you
48 | use regex captures to extract the identifier. The regex in the other methods are designed to capture
49 | the identifier. Basically, a parenthesis represents a capture. When you run `exec`, it gives you an
50 | array of all the captures. The first element is always the string you pass to `exec`. The second
51 | element will be the identifier. So you need to use index `1` to retrieve the identifier.
52 | After you retrieve the identifier, everything else is pretty much the same as other methods.
53 |
--------------------------------------------------------------------------------
/src/workspace/common/WorkspaceTableHead.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import TableHead from "@material-ui/core/TableHead";
4 | import TableRow from "@material-ui/core/TableRow";
5 | import TableCell from "@material-ui/core/TableCell";
6 | // import Checkbox from "@material-ui/core/Checkbox";
7 | import TableSortLabel from "@material-ui/core/TableSortLabel";
8 |
9 | export default function WorkspaceTableHead(props) {
10 | const {
11 | classes,
12 | // onSelectAll,
13 | order,
14 | orderBy,
15 | // selectionCount,
16 | // rowCount,
17 | onRequestSort,
18 | headers,
19 | } = props;
20 | const createSortHandler = (property) => (event) => {
21 | onRequestSort(event, property);
22 | };
23 |
24 | return (
25 |
26 |
27 | {/*
28 | 0 && selectionCount < rowCount
31 | }
32 | checked={rowCount > 0 && selectionCount === rowCount}
33 | onChange={onSelectAll}
34 | />
35 | */}
36 | {headers.map((header) => (
37 |
43 |
48 | {header.label}
49 | {orderBy === header.id ? (
50 |
51 | {order === "desc"
52 | ? "sorted descending"
53 | : "sorted ascending"}
54 |
55 | ) : null}
56 |
57 |
58 | ))}
59 |
60 |
61 | );
62 | }
63 |
64 | WorkspaceTableHead.propTypes = {
65 | classes: PropTypes.object.isRequired,
66 | selectionCount: PropTypes.number.isRequired,
67 | onRequestSort: PropTypes.func.isRequired,
68 | onSelectAll: PropTypes.func.isRequired,
69 | order: PropTypes.oneOf(["asc", "desc"]).isRequired,
70 | orderBy: PropTypes.string.isRequired,
71 | rowCount: PropTypes.number.isRequired,
72 | };
73 |
--------------------------------------------------------------------------------
/src/workspace/invoice/ViewInvoice.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import CircularProgress from "@material-ui/core/CircularProgress";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import { connect } from "react-redux";
5 | import { useParams, withRouter } from "react-router-dom";
6 |
7 | import WorkspaceToolbar from "../common/WorkspaceToolbar";
8 | import InvoiceCard from "./InvoiceCard";
9 | import * as actions from "../../redux/actions";
10 |
11 | import DownloadIcon from "@material-ui/icons/GetApp";
12 | import EditIcon from "@material-ui/icons/Edit";
13 |
14 | const useStyles = makeStyles((theme) => ({
15 | container: {
16 | padding: 16,
17 | },
18 | InvoiceCard: {
19 | maxWidth: 600,
20 | },
21 | section: {
22 | marginBottom: 32,
23 | },
24 | sectionTitle: {
25 | fontSize: 20,
26 | marginBottom: 16,
27 | fontWeight: 500,
28 | },
29 | subscriptions: {},
30 | progress: {
31 | position: "fixed",
32 | top: "50%",
33 | left: "50%",
34 | marginTop: -24,
35 | marginLeft: -24,
36 | },
37 | }));
38 |
39 | const primaryActions = [
40 | {
41 | identifier: "edit",
42 | title: "Edit",
43 | icon: EditIcon,
44 | primary: true,
45 | },
46 | {
47 | identifier: "download",
48 | title: "Download",
49 | icon: DownloadIcon,
50 | primary: true,
51 | },
52 | ];
53 |
54 | function ViewInvoice(props) {
55 | const classes = useStyles();
56 | const { fetchInvoice, clearInvoice, invoice, editInvoice } = props;
57 | const { identifier } = useParams();
58 |
59 | const handleAction = (type) => {
60 | if (type === "edit") {
61 | editInvoice(invoice);
62 | }
63 | };
64 |
65 | useEffect(() => {
66 | fetchInvoice(identifier);
67 | return clearInvoice;
68 | }, [identifier, fetchInvoice, clearInvoice]);
69 |
70 | return (
71 |
72 |
77 | {!invoice && (
78 |
79 | )}
80 | {invoice && (
81 |
82 |
86 |
87 | )}
88 |
89 | );
90 | }
91 |
92 | function mapStateToProps(state) {
93 | return {
94 | invoice: state.invoice,
95 | };
96 | }
97 |
98 | const mapDispatchToProps = {
99 | fetchInvoice: actions.fetchInvoice,
100 | clearInvoice: actions.clearInvoice,
101 | editInvoice: actions.editInvoice,
102 | };
103 |
104 | export default connect(
105 | mapStateToProps,
106 | mapDispatchToProps
107 | )(withRouter(ViewInvoice));
108 |
--------------------------------------------------------------------------------
/src/workspace/analytics/LineGraph.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | ResponsiveContainer,
4 | AreaChart,
5 | Area,
6 | XAxis,
7 | YAxis,
8 | CartesianGrid,
9 | Tooltip,
10 | } from "recharts";
11 | import { makeStyles } from "@material-ui/core/styles";
12 | import Typography from "@material-ui/core/Typography";
13 | import Paper from "@material-ui/core/Paper";
14 |
15 | const useStyles = makeStyles((theme) => ({
16 | style: {
17 | margin: 8,
18 | marginBottom: 48,
19 | width: "auto",
20 | height: 220,
21 | },
22 | tooltip: {
23 | justifyContent: "center",
24 | padding: 8,
25 | },
26 | item: {
27 | margin: 2,
28 | },
29 | subtitle: {
30 | color: "#777777",
31 | },
32 | }));
33 |
34 | const months = {
35 | Jan: "January",
36 | Feb: "February",
37 | Mar: "March",
38 | Apr: "April",
39 | May: "May",
40 | Jun: "June",
41 | Jul: "July",
42 | Aug: "August",
43 | Sep: "September",
44 | Oct: "October",
45 | Nov: "November",
46 | Dec: "December",
47 | };
48 |
49 | function LineGraph(props, theme) {
50 | const classes = useStyles();
51 |
52 | const { title, name, color, dataKey, info, data } = props;
53 |
54 | const CustomTooltip = ({ active, payload, label }) => {
55 | if (active && payload) {
56 | return (
57 |
58 | {`Month: ${months[label]}`}
61 |
62 |
63 | {`${name}: ${payload[0].value}`}
64 |
65 |
66 | );
67 | }
68 | return null;
69 | };
70 | return (
71 |
72 |
73 | {title}
74 | {info}
75 |
76 |
77 |
86 |
87 |
88 |
89 | } />
90 |
97 |
98 |
99 |
100 | );
101 | }
102 |
103 | export default LineGraph;
104 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
2 |
3 | ## Available Scripts
4 |
5 | In the project directory, you can run:
6 |
7 | ### `yarn start`
8 |
9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
11 |
12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console.
14 |
15 | ### `yarn test`
16 |
17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
19 |
20 | ### `yarn build`
21 |
22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance.
24 |
25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed!
27 |
28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
29 |
30 | ### `yarn eject`
31 |
32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!**
33 |
34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
35 |
36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
37 |
38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
39 |
40 | ## Learn More
41 |
42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
43 |
44 | To learn React, check out the [React documentation](https://reactjs.org/).
45 |
46 | ### Code Splitting
47 |
48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
49 |
50 | ### Analyzing the Bundle Size
51 |
52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
53 |
54 | ### Making a Progressive Web App
55 |
56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
57 |
58 | ### Advanced Configuration
59 |
60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
61 |
62 | ### Deployment
63 |
64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
65 |
66 | ### `yarn build` fails to minify
67 |
68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify
69 |
70 | ## Prettier Integration
71 |
72 | We use Prettier with Pretty Quick and the pre-commit tool Husky. This reformats our files that are marked as "staged" via `git add` before committing.
73 |
--------------------------------------------------------------------------------
/src/workspace/transaction/ViewTransaction.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import CircularProgress from "@material-ui/core/CircularProgress";
3 | import Typography from "@material-ui/core/Typography";
4 | import { makeStyles } from "@material-ui/core/styles";
5 | import { connect } from "react-redux";
6 | import { useParams, withRouter } from "react-router-dom";
7 |
8 | import WorkspaceToolbar from "../common/WorkspaceToolbar";
9 | import TransactionCard from "../transaction/TransactionCard";
10 | import * as actions from "../../redux/actions";
11 |
12 | const useStyles = makeStyles((theme) => ({
13 | container: {
14 | padding: 16,
15 | },
16 | transactionCard: {
17 | maxWidth: 600,
18 | },
19 | section: {
20 | marginBottom: 32,
21 | },
22 | sectionTitle: {
23 | fontSize: 20,
24 | marginBottom: 16,
25 | fontWeight: 500,
26 | },
27 | transactions: {},
28 | progress: {
29 | position: "fixed",
30 | top: "50%",
31 | left: "50%",
32 | marginTop: -24,
33 | marginLeft: -24,
34 | },
35 | }));
36 |
37 | function ViewTransaction(props) {
38 | const classes = useStyles();
39 | const {
40 | transaction,
41 | fetchTransaction,
42 | clearTransaction,
43 | editTransaction,
44 | } = props;
45 | const { identifier } = useParams();
46 |
47 | const handleEditTransaction = () => {
48 | editTransaction(transaction);
49 | };
50 |
51 | useEffect(() => {
52 | fetchTransaction(identifier);
53 | return clearTransaction;
54 | }, [identifier, fetchTransaction, clearTransaction]);
55 |
56 | return (
57 |
58 |
59 | {!transaction && (
60 |
61 | )}
62 | {transaction && (
63 |
64 |
65 |
69 | General
70 |
71 |
82 |
83 |
84 | )}
85 |
86 | );
87 | }
88 |
89 | function mapStateToProps(state) {
90 | return {
91 | transaction: state.transaction,
92 | };
93 | }
94 |
95 | const mapDispatchToProps = {
96 | fetchTransaction: actions.fetchTransaction,
97 | clearTransaction: actions.clearTransaction,
98 | editTransaction: actions.editTransaction,
99 | };
100 |
101 | export default connect(
102 | mapStateToProps,
103 | mapDispatchToProps
104 | )(withRouter(ViewTransaction));
105 |
--------------------------------------------------------------------------------
/src/workspace/plan/ViewPlan.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import CircularProgress from "@material-ui/core/CircularProgress";
3 | import Typography from "@material-ui/core/Typography";
4 | import { makeStyles } from "@material-ui/core/styles";
5 | import { connect } from "react-redux";
6 | import { useParams, withRouter } from "react-router-dom";
7 |
8 | import WorkspaceToolbar from "../common/WorkspaceToolbar";
9 | import PlanCard from "./PlanCard";
10 | import * as actions from "../../redux/actions";
11 |
12 | const useStyles = makeStyles((theme) => ({
13 | container: {
14 | padding: 16,
15 | },
16 | planCard: {
17 | maxWidth: 600,
18 | },
19 | section: {
20 | marginBottom: 32,
21 | },
22 | sectionTitle: {
23 | fontSize: 20,
24 | marginBottom: 16,
25 | fontWeight: 500,
26 | },
27 | progress: {
28 | position: "fixed",
29 | top: "50%",
30 | left: "50%",
31 | marginTop: -24,
32 | marginLeft: -24,
33 | },
34 | }));
35 |
36 | function ViewPlan(props) {
37 | const classes = useStyles();
38 | const { plan, fetchPlan, clearPlan, editPlan } = props;
39 | const { identifier } = useParams();
40 |
41 | const handleEdit = () => {
42 | editPlan(plan);
43 | };
44 |
45 | useEffect(() => {
46 | fetchPlan(identifier);
47 | return clearPlan;
48 | }, [identifier, fetchPlan, clearPlan]);
49 |
50 | console.log(plan);
51 |
52 | return (
53 |
54 |
55 | {!plan && (
56 |
57 | )}
58 | {plan && (
59 |
60 |
61 |
65 | General
66 |
67 |
83 |
84 |
85 | )}
86 |
87 | );
88 | }
89 |
90 | function mapStateToProps(state) {
91 | return {
92 | plan: state.plan,
93 | };
94 | }
95 |
96 | const mapDispatchToProps = {
97 | fetchPlan: actions.fetchPlan,
98 | clearPlan: actions.clearPlan,
99 | editPlan: actions.editPlan,
100 | };
101 |
102 | export default connect(
103 | mapStateToProps,
104 | mapDispatchToProps
105 | )(withRouter(ViewPlan));
106 |
--------------------------------------------------------------------------------
/src/workspace/analytics/BarGraph.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import {
4 | ResponsiveContainer,
5 | BarChart,
6 | Bar,
7 | XAxis,
8 | YAxis,
9 | CartesianGrid,
10 | Tooltip,
11 | Legend,
12 | } from "recharts";
13 | import Typography from "@material-ui/core/Typography";
14 | import Paper from "@material-ui/core/Paper";
15 |
16 | const useStyles = makeStyles((theme) => ({
17 | style: {
18 | margin: 16,
19 | marginBottom: 88,
20 | width: "auto",
21 | height: 240,
22 | justifyContent: "center",
23 | },
24 | tooltip: {
25 | justifyContent: "center",
26 | padding: 8,
27 | },
28 | item: {
29 | margin: 2,
30 | },
31 | parent: {
32 | display: "flex",
33 | flexDirection: "row",
34 | },
35 | subtitle: {
36 | color: "#777777",
37 | },
38 | }));
39 |
40 | const months = {
41 | Jan: "January",
42 | Feb: "February",
43 | Mar: "March",
44 | Apr: "April",
45 | May: "May",
46 | Jun: "June",
47 | Jul: "July",
48 | Aug: "August",
49 | Sep: "September",
50 | Oct: "October",
51 | Nov: "November",
52 | Dec: "December",
53 | };
54 |
55 | const colors = {
56 | blues: ["#90CAF9", "#42A5F5", "#1E88E5", "#1565C0", "#0D47A1"],
57 | reds: ["#FFCDD2", "#E57373", "#F44336", "#D32F2F", "#B71C1C"],
58 | purples: ["#E1BEE7", "#BA68C8", "#9C27B0", "#7B1FA2", "#4A148C"],
59 | oranges: ["#FFE0B2", "#FFB74D", "#FF9800", "#F57C00", "#E65100"],
60 | };
61 |
62 | function BarGraph(props, theme) {
63 | const classes = useStyles();
64 | const { title, names, info, data, keys, color } = props;
65 |
66 | const CustomTooltip = ({ active, payload, label }) => {
67 | if (active && payload) {
68 | return (
69 |
70 |
71 | {`Month: ${months[label]}`}
72 |
73 |
74 | {props.names.map((name, index) => (
75 |
76 |
80 | ◼
81 |
82 |
83 | {`${name}: ${payload[index].value}`}
84 |
85 |
86 | ))}
87 |
88 | );
89 | }
90 | return null;
91 | };
92 |
93 | return (
94 |
95 |
96 | {title}
97 | {info}
98 |
99 |
100 |
109 |
110 |
111 |
112 | } />
113 |
114 | {keys.map((bar, index) => (
115 |
121 | ))}
122 |
123 |
124 |
125 | );
126 | }
127 |
128 | export default BarGraph;
129 |
--------------------------------------------------------------------------------
/src/workspace/subscription/ViewSubscription.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import CircularProgress from "@material-ui/core/CircularProgress";
3 | import Typography from "@material-ui/core/Typography";
4 | import { makeStyles } from "@material-ui/core/styles";
5 | import { connect } from "react-redux";
6 | import { useParams, withRouter } from "react-router-dom";
7 |
8 | import WorkspaceToolbar from "../common/WorkspaceToolbar";
9 | import SubscriptionCard from "../subscription/SubscriptionCard";
10 | import * as actions from "../../redux/actions";
11 |
12 | const useStyles = makeStyles((theme) => ({
13 | container: {
14 | padding: 16,
15 | },
16 | subscriptionCard: {
17 | maxWidth: 600,
18 | },
19 | section: {
20 | marginBottom: 32,
21 | },
22 | sectionTitle: {
23 | fontSize: 20,
24 | marginBottom: 16,
25 | fontWeight: 500,
26 | },
27 | subscriptions: {},
28 | progress: {
29 | position: "fixed",
30 | top: "50%",
31 | left: "50%",
32 | marginTop: -24,
33 | marginLeft: -24,
34 | },
35 | }));
36 |
37 | // Some fields aren't being rendered.
38 | function ViewSubscription(props) {
39 | const classes = useStyles();
40 | const { fetchSubscription, clearSubscription, subscription } = props;
41 | const { identifier } = useParams();
42 |
43 | useEffect(() => {
44 | fetchSubscription(identifier);
45 | return clearSubscription;
46 | }, [identifier, fetchSubscription, clearSubscription]);
47 |
48 | return (
49 |
50 |
51 | {!subscription && (
52 |
53 | )}
54 | {subscription && (
55 |
56 |
57 |
61 | General
62 |
63 |
90 |
91 |
92 | )}
93 |
94 | );
95 | }
96 |
97 | function mapStateToProps(state) {
98 | return {
99 | subscription: state.subscription,
100 | };
101 | }
102 |
103 | const mapDispatchToProps = {
104 | fetchSubscription: actions.fetchSubscription,
105 | clearSubscription: actions.clearSubscription,
106 | };
107 |
108 | export default connect(
109 | mapStateToProps,
110 | mapDispatchToProps
111 | )(withRouter(ViewSubscription));
112 |
--------------------------------------------------------------------------------
/src/workspace/transaction/TransactionCard.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import clsx from "clsx";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import Card from "@material-ui/core/Card";
5 | import CardHeader from "@material-ui/core/CardHeader";
6 | import CardContent from "@material-ui/core/CardContent";
7 | import Avatar from "@material-ui/core/Avatar";
8 | import Grid from "@material-ui/core/Grid";
9 | import Typography from "@material-ui/core/Typography";
10 | import { green } from "@material-ui/core/colors";
11 | import { toDateString } from "../../utils";
12 |
13 | const useStyles = makeStyles((theme) => ({
14 | root: {},
15 | avatar: {
16 | backgroundColor: green[500],
17 | },
18 | title: {
19 | fontWeight: 600,
20 | fontSize: 14,
21 | },
22 | value: {
23 | fontSize: 15,
24 | },
25 | icon: {
26 | display: "inline-block",
27 | marginRight: 4,
28 | },
29 | button: {
30 | color: theme.palette.primary.main,
31 | marginLeft: "auto",
32 | },
33 | }));
34 |
35 | const actionNames = {
36 | purchase: "Purchase",
37 | verify: "Verification",
38 | refund: "Refund",
39 | };
40 |
41 | const methodNames = {
42 | cash: "Cash",
43 | credit_card: "Credit Card",
44 | debit_card: "Debit Card",
45 | online: "Online / Netbanking",
46 | };
47 |
48 | const fields = [
49 | {
50 | identifier: "referenceId",
51 | title: "Reference ID",
52 | size: 6,
53 | render: (transaction) => transaction.referenceId,
54 | },
55 | {
56 | identifier: "action",
57 | title: "Action",
58 | size: 6,
59 | render: (transaction) => actionNames[transaction.action],
60 | },
61 | {
62 | identifier: "paymentMethod",
63 | title: "Payment Method",
64 | size: 6,
65 | render: (transaction) => methodNames[transaction.paymentMethod],
66 | },
67 | {
68 | identifier: "createdAt",
69 | title: "Transaction Date",
70 | size: 6,
71 | render: (transaction) =>
72 | toDateString(new Date(transaction.createdAt.substring(0, 10))),
73 | },
74 | {
75 | identifier: "amount",
76 | title: "Amount",
77 | size: 6,
78 | render: (transaction) =>
79 | transaction.amount ? transaction.amount + " INR" : "Unavailable",
80 | },
81 | {
82 | identifier: "tax",
83 | title: "Tax",
84 | size: 6,
85 | render: (transaction) =>
86 | transaction.tax ? transaction.tax + " INR" : "Unavailable",
87 | },
88 | {
89 | identifier: "total",
90 | title: "Total Amount",
91 | size: 12,
92 | render: (transaction) =>
93 | transaction.amount && transaction.tax
94 | ? transaction.amount + transaction.tax + " INR"
95 | : "Unavailable",
96 | },
97 | ];
98 |
99 | function TransactionCard(props) {
100 | const classes = useStyles();
101 | const { className } = props;
102 | return (
103 |
104 | ₹}
106 | title="Transaction"
107 | />
108 |
109 |
110 |
111 | {fields.map((field) => (
112 |
113 |
114 |
119 | {field.title}
120 |
121 |
122 | {field.render(props)}
123 |
124 |
125 |
126 | ))}
127 |
128 |
129 |
130 | {/*
131 |
132 |
133 | Edit
134 |
135 | */}
136 |
137 | );
138 | }
139 |
140 | export default TransactionCard;
141 |
--------------------------------------------------------------------------------
/src/layouts/MainToolbar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | AppBar,
4 | Button,
5 | Toolbar,
6 | makeStyles,
7 | IconButton,
8 | } from "@material-ui/core";
9 | import { withRouter } from "react-router-dom";
10 | import clsx from "clsx";
11 | import { connect } from "react-redux";
12 |
13 | import AddIcon from "@material-ui/icons/Add";
14 | import MenuIcon from "@material-ui/icons/Menu";
15 | import LogoutIcon from "@material-ui/icons/ExitToApp";
16 |
17 | import AddDialog from "./AddDialog";
18 | import * as actions from "../redux/actions";
19 |
20 | const drawerWidth = 240;
21 |
22 | const useStyles = makeStyles((theme) => ({
23 | logoButton: {
24 | height: 64,
25 | width: 180,
26 | borderRadius: 0,
27 | },
28 | appBar: {
29 | zIndex: theme.zIndex.drawer + 1,
30 | transition: theme.transitions.create(["width", "margin"], {
31 | easing: theme.transitions.easing.sharp,
32 | duration: theme.transitions.duration.leavingScreen,
33 | }),
34 | color: theme.palette.primary,
35 | background: "white",
36 | },
37 | appBarShift: {
38 | marginLeft: drawerWidth,
39 | width: `calc(100% - ${drawerWidth}px)`,
40 | transition: theme.transitions.create(["width", "margin"], {
41 | easing: theme.transitions.easing.sharp,
42 | duration: theme.transitions.duration.enteringScreen,
43 | }),
44 | },
45 | toolbar: {},
46 | hide: {
47 | display: "none",
48 | },
49 | buttons: {
50 | marginLeft: "auto",
51 | },
52 | addButton: {
53 | paddingLeft: 16,
54 | paddingRight: 16,
55 | },
56 | logoutButton: {
57 | marginLeft: 8,
58 | paddingLeft: 16,
59 | paddingRight: 16,
60 | },
61 | icon: {
62 | dispay: "inline-block",
63 | marginRight: 4,
64 | },
65 | }));
66 |
67 | function MainToolbar(props) {
68 | const { toggleDrawer, drawerOpen, user, logout } = props;
69 | const classes = useStyles();
70 | const [addDialogAnchor, setAddDialogAnchor] = React.useState(null);
71 |
72 | const handleOpenAddDialog = (event) =>
73 | setAddDialogAnchor(event.currentTarget);
74 | const handleCloseAddDialog = () => setAddDialogAnchor(null);
75 |
76 | return (
77 |
83 |
84 |
89 |
90 |
91 |
92 |
93 |
98 |
99 |
100 |
107 |
108 | Quick Add
109 |
110 |
111 |
118 |
119 | Logout {user.firstName}
120 |
121 |
122 |
123 |
128 |
129 |
130 | );
131 | }
132 |
133 | function mapStateToProps(state) {
134 | return {
135 | user: state.user,
136 | };
137 | }
138 |
139 | const mapDispatchToProps = {
140 | fetchUser: actions.fetchUser,
141 | logout: actions.logout,
142 | };
143 |
144 | export default connect(
145 | mapStateToProps,
146 | mapDispatchToProps
147 | )(withRouter(MainToolbar));
148 |
--------------------------------------------------------------------------------
/src/workspace/api-key/APIKeyFormDrawer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { extractValues } from "../common/RecordForm";
5 |
6 | import FormDrawer from "../common/FormDrawer";
7 |
8 | // text, large_text, number, date_picker, date_range_picker, switch, phone_number, email_address
9 | // multiple_options (multiselect), single_option (drop down)
10 | // lookup - organization, user, account
11 |
12 | // Only top level children can have quickAdd. Groups cannot have required, unique, multipleValues, mininmumValues, maximumValues.
13 | // Groups can have readOnly, hidden, tooltip
14 | // The same person can work in multiple organizations. But such cases are rare. Therefore, the system should be kept
15 | // simple and not accomodate such cases. given, there are other work arounds.
16 |
17 | // The user name should be unique across your organization.
18 | const groups = [
19 | {
20 | label: "Basic",
21 | children: [
22 | {
23 | label: "Name",
24 | identifier: "name",
25 | type: "text",
26 | required: true,
27 | readOnly: false,
28 | quickAdd: true,
29 | unique: true,
30 | hidden: false,
31 | tooltip: "The name of the API key.",
32 | multipleValues: false,
33 | defaultValue: "",
34 | },
35 | {
36 | label: "Read Permissions",
37 | identifier: "readPermisions",
38 | type: "multi_select",
39 | options: [
40 | {
41 | value: "READ_ACCOUNTS",
42 | title: "Accounts",
43 | },
44 | {
45 | value: "READ_SUBSCRIPTIONS",
46 | title: "Subscriptions",
47 | },
48 | {
49 | value: "READ_INVOICES",
50 | title: "Invoices",
51 | },
52 | {
53 | value: "READ_TRANSACTIONS",
54 | title: "Transactions",
55 | },
56 | ],
57 | required: true,
58 | readOnly: false,
59 | quickAdd: true,
60 | unique: false,
61 | hidden: false,
62 | tooltip:
63 | "The resources that the API key provides read access to.",
64 | multipleValues: false,
65 | defaultValue: [],
66 | },
67 | {
68 | label: "Read Write Permissions",
69 | identifier: "readWritePermisions",
70 | type: "multi_select",
71 | options: [
72 | {
73 | value: "READ_WRITE_ACCOUNTS",
74 | title: "Accounts",
75 | },
76 | {
77 | value: "READ_WRITE_SUBSCRIPTIONS",
78 | title: "Subscriptions",
79 | },
80 | {
81 | value: "READ_WRITE_INVOICES",
82 | title: "Invoices",
83 | },
84 | {
85 | value: "READ_WRITE_TRANSACTIONS",
86 | title: "Transactions",
87 | },
88 | ],
89 | required: true,
90 | readOnly: false,
91 | quickAdd: true,
92 | unique: false,
93 | hidden: false,
94 | tooltip:
95 | "The resources that the API key provides read-write access to.",
96 | multipleValues: false,
97 | defaultValue: [],
98 | },
99 | ],
100 | },
101 | ];
102 |
103 | function APIKeyFormDrawer(props) {
104 | const { title, onSave, showMore, open } = props;
105 |
106 | const values = props.account || extractValues(groups);
107 | return (
108 |
116 | );
117 | }
118 |
119 | APIKeyFormDrawer.propTypes = {
120 | title: PropTypes.string.isRequired,
121 | showMore: PropTypes.bool,
122 | account: PropTypes.object,
123 | onSave: PropTypes.func.isRequired,
124 | };
125 |
126 | APIKeyFormDrawer.defaultProps = {
127 | showMore: false,
128 | account: null,
129 | onCancel: null,
130 | };
131 |
132 | export default APIKeyFormDrawer;
133 |
--------------------------------------------------------------------------------
/src/workspace/plan/PlanCard.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import clsx from "clsx";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import Card from "@material-ui/core/Card";
5 | import CardHeader from "@material-ui/core/CardHeader";
6 | import CardContent from "@material-ui/core/CardContent";
7 | import Avatar from "@material-ui/core/Avatar";
8 | import Grid from "@material-ui/core/Grid";
9 | import Typography from "@material-ui/core/Typography";
10 | import { green } from "@material-ui/core/colors";
11 | import { toDateString } from "../../utils";
12 |
13 | const useStyles = makeStyles((theme) => ({
14 | root: {},
15 | avatar: {
16 | backgroundColor: green[500],
17 | },
18 | title: {
19 | fontWeight: 600,
20 | fontSize: 14,
21 | },
22 | value: {
23 | fontSize: 15,
24 | },
25 | icon: {
26 | display: "inline-block",
27 | marginRight: 4,
28 | },
29 | close: {
30 | marginLeft: "auto",
31 | color: theme.palette.error.main,
32 | paddingLeft: 16,
33 | paddingRight: 16,
34 | },
35 | edit: {
36 | marginLeft: "auto",
37 | color: theme.palette.primary.main,
38 | paddingLeft: 16,
39 | paddingRight: 16,
40 | },
41 | }));
42 |
43 | const fields = [
44 | {
45 | identifier: "createdAt",
46 | title: "Created At",
47 | size: 6,
48 | render: (props) => toDateString(props.createdAt),
49 | },
50 | {
51 | identifier: "billingCyclePeriod",
52 | title: "Billing Cycle Period",
53 | size: 6,
54 | render: (props) =>
55 | props.billingCyclePeriod
56 | ? props.billingCyclePeriod + " " + props.billingCyclePeriodUnit
57 | : "Unavailable",
58 | },
59 | {
60 | identifier: "pricePerBillingCycle",
61 | title: "Price",
62 | size: 6,
63 | render: (props) =>
64 | props.pricePerBillingCycle
65 | ? props.pricePerBillingCycle + " INR"
66 | : "Unavailable",
67 | },
68 | {
69 | identifier: "setupFee",
70 | title: "Setup Fee",
71 | size: 6,
72 | render: (props) =>
73 | props.setupFee ? props.setupFee + " INR" : "Unavailable",
74 | },
75 | {
76 | identifier: "trialPeriod",
77 | title: "Trial Period",
78 | size: 6,
79 | render: (props) =>
80 | props.trialPeriod
81 | ? props.trialPeriod + " " + props.trialPeriodUnit
82 | : "Unavailable",
83 | },
84 | {
85 | identifier: "totalBillingCycles",
86 | title: "Total Billing Cycles",
87 | size: 6,
88 | render: (props) => props.totalBillingCycles,
89 | },
90 | {
91 | identifier: "renew",
92 | title: "Renews",
93 | size: 6,
94 | render: (props) => (props.renews ? "Yes" : "No"),
95 | },
96 | ];
97 |
98 | function PlanCard(props) {
99 | const classes = useStyles();
100 | const { className, name, code, description } = props;
101 |
102 | return (
103 |
104 |
107 | {name.substring(0, 1).toUpperCase()}
108 |
109 | }
110 | title={name}
111 | subheader={code}
112 | subtitle={description}
113 | />
114 |
115 |
116 |
117 | {fields.map((field) => (
118 |
119 |
120 |
125 | {field.title}
126 |
127 |
128 | {field.render(props)}
129 |
130 |
131 |
132 | ))}
133 |
134 |
135 |
136 | {/*
137 |
138 |
139 | Edit
140 |
141 | */}
142 |
143 | );
144 | }
145 |
146 | export default PlanCard;
147 |
--------------------------------------------------------------------------------
/src/workspace/common/WorkspaceToolbar.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import clsx from "clsx";
4 | import { lighten, makeStyles } from "@material-ui/core/styles";
5 | import AppBar from "@material-ui/core/AppBar";
6 | import Button from "@material-ui/core/Button";
7 | import Toolbar from "@material-ui/core/Toolbar";
8 | import Typography from "@material-ui/core/Typography";
9 |
10 | const useToolbarStyles = makeStyles((theme) => ({
11 | appBar: {
12 | color: theme.palette.primary,
13 | background: "white",
14 | },
15 | toolbar: {
16 | paddingLeft: theme.spacing(2),
17 | paddingRight: theme.spacing(1),
18 | },
19 | buttons: {
20 | marginLeft: "auto",
21 | },
22 | action: {
23 | textTransform: "none",
24 | paddingLeft: 16,
25 | paddingRight: 16,
26 | },
27 | title: {
28 | color: theme.palette.text.primary,
29 | fontWeight: 500,
30 | },
31 | highlight:
32 | theme.palette.type === "light"
33 | ? {
34 | color: theme.palette.secondary.main,
35 | backgroundColor: lighten(theme.palette.secondary.light, 0.85),
36 | }
37 | : {
38 | color: theme.palette.text.primary,
39 | backgroundColor: theme.palette.secondary.dark,
40 | },
41 | actionIcon: {
42 | marginRight: 4,
43 | },
44 | }));
45 |
46 | function WorkspaceToolbar(props) {
47 | const classes = useToolbarStyles();
48 | const { title, selectionCount, actions, onAction } = props;
49 |
50 | const makeActionHandler = (identifier) => () => onAction(identifier);
51 |
52 | const normalTitle = (
53 |
59 | {title}
60 |
61 | );
62 |
63 | const selectedTitle = (
64 |
69 | {selectionCount} selected
70 |
71 | );
72 |
73 | const selected = selectionCount > 0;
74 |
75 | const recordButtons = () => {
76 | if (actions) {
77 | return actions.map(
78 | (action) =>
79 | !action.primary && (
80 |
87 |
88 | {action.title}
89 |
90 | )
91 | );
92 | }
93 | return null;
94 | };
95 |
96 | const normalButtons = () => {
97 | if (actions) {
98 | return (
99 |
100 | {actions.map(
101 | (action) =>
102 | action.primary && (
103 |
112 |
115 | {action.title}
116 |
117 | )
118 | )}
119 |
120 | );
121 | }
122 | return null;
123 | };
124 |
125 | return (
126 |
127 |
132 | {selected ? selectedTitle : normalTitle}
133 |
134 |
135 | {selected ? recordButtons() : normalButtons()}
136 |
137 |
138 |
139 | );
140 | }
141 |
142 | WorkspaceToolbar.propTypes = {
143 | selectionCount: PropTypes.number.isRequired,
144 | };
145 |
146 | export default WorkspaceToolbar;
147 |
--------------------------------------------------------------------------------
/src/layouts/AddDialog.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import Typography from "@material-ui/core/Typography";
5 | import Icon from "@material-ui/core/Icon";
6 | import Grid from "@material-ui/core/Grid";
7 | import Popover from "@material-ui/core/Popover";
8 |
9 | import { connect } from "react-redux";
10 | import {
11 | newAccount,
12 | newSubscription,
13 | newInvoice,
14 | newTransaction,
15 | newPlan,
16 | newAPIKey,
17 | } from "../redux/actions";
18 |
19 | const useStyles = makeStyles((theme) => ({
20 | root: {
21 | minHeight: 260,
22 | minWidth: 400,
23 | overflow: "hidden",
24 | },
25 | main: {
26 | padding: 16,
27 | },
28 | groupTitle: {
29 | fontSize: 12,
30 | backgroundColor: "#F0F0F0",
31 | padding: 8,
32 | paddingLeft: 16,
33 | marginTop: 0,
34 | marginBottom: 0,
35 | textTransform: "uppercase",
36 | },
37 | icon: {
38 | display: "block !important",
39 | marginLeft: "auto !important",
40 | marginRight: "auto !important",
41 | },
42 | link: {
43 | color: "black",
44 | textDecoration: "none",
45 | },
46 | add: {
47 | padding: 12,
48 | "&:hover": {
49 | background: "#D3D3D3",
50 | },
51 | width: 100,
52 | borderRadius: 4,
53 | cursor: "pointer",
54 | },
55 | linkTitle: {
56 | marginTop: 4,
57 | textAlign: "center",
58 | fontSize: 12,
59 | },
60 | }));
61 |
62 | const groups = [
63 | {
64 | title: "Record",
65 | links: [
66 | {
67 | id: "account",
68 | title: "Account",
69 | icon: "account_circle",
70 | action: "newAccount",
71 | },
72 | {
73 | id: "subscription",
74 | title: "Subscription",
75 | icon: "autorenew",
76 | action: "newSubscription",
77 | },
78 | {
79 | id: "transaction",
80 | title: "Transaction",
81 | icon: "monetization_on",
82 | action: "newTransaction",
83 | },
84 | ],
85 | },
86 | {
87 | title: "Configuration",
88 | links: [
89 | {
90 | id: "plan",
91 | title: "Plan",
92 | icon: "local_offer",
93 | action: "newPlan",
94 | },
95 | {
96 | id: "api-key",
97 | title: "API Key",
98 | icon: "code",
99 | action: "newAPIKey",
100 | },
101 | ],
102 | },
103 | ];
104 |
105 | function AddDialog(props) {
106 | const classes = useStyles();
107 | const { onClose, open, anchor } = props;
108 | const makeHandler = (link) => () => {
109 | onClose();
110 | props[link.action]();
111 | };
112 |
113 | return (
114 |
127 |
128 | {groups.map((group, index) => (
129 |
130 |
{group.title}
131 |
136 | {group.links.map((link, index) => (
137 |
138 |
142 |
143 | {link.icon}
144 |
145 |
148 | {link.title}
149 |
150 |
151 |
152 | ))}
153 |
154 |
155 | ))}
156 |
157 |
158 | );
159 | }
160 |
161 | AddDialog.propTypes = {
162 | onClose: PropTypes.func.isRequired,
163 | open: PropTypes.bool.isRequired,
164 | selectedValue: PropTypes.string.isRequired,
165 | };
166 |
167 | const mapDispatchToProps = {
168 | newAccount,
169 | newSubscription,
170 | newInvoice,
171 | newTransaction,
172 | newPlan,
173 | newAPIKey,
174 | };
175 |
176 | export default connect(null, mapDispatchToProps)(AddDialog);
177 |
--------------------------------------------------------------------------------
/src/workspace/transaction/TransactionFormDrawer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { extractValues } from "../common/RecordForm";
5 | import FormDrawer from "../common/FormDrawer";
6 |
7 | const groups = [
8 | {
9 | label: "Basic",
10 | children: [
11 | {
12 | label: "Reference ID",
13 | identifier: "referenceId",
14 | type: "text",
15 | required: true,
16 | readOnly: false,
17 | quickAdd: true,
18 | unique: false,
19 | hidden: false,
20 | tooltip: "Reference ID of the transaction.",
21 | multipleValues: false,
22 | defaultValue: "",
23 | },
24 | {
25 | label: "Comments",
26 | identifier: "comments",
27 | type: "large_text",
28 | required: false,
29 | readOnly: false,
30 | quickAdd: true,
31 | unique: false,
32 | hidden: false,
33 | tooltip: "Any comments on the transaction.",
34 | multipleValues: false,
35 | defaultValue: "",
36 | rows: 4,
37 | validations: "maxLength:200",
38 | validationErrors: {
39 | maxLength: "The comment must be 0-200 characters long.",
40 | },
41 | },
42 | {
43 | label: "Amount",
44 | identifier: "amount",
45 | type: "number",
46 | required: false,
47 | readOnly: false,
48 | quickAdd: true,
49 | unique: false,
50 | hidden: false,
51 | tooltip: "Amount of the transaction.",
52 | multipleValues: false,
53 | defaultValue: 0,
54 | validations: "isNumeric",
55 | validationErrors: {
56 | isNumeric: "Please enter a valid number.",
57 | },
58 | },
59 | {
60 | label: "Tax",
61 | identifier: "tax",
62 | type: "number",
63 | required: false,
64 | readOnly: false,
65 | quickAdd: true,
66 | unique: false,
67 | hidden: false,
68 | tooltip: "Tax of the transaction.",
69 | multipleValues: false,
70 | defaultValue: 0,
71 | validations: "isNumeric",
72 | validationErrors: {
73 | isNumeric: "Please enter a valid number.",
74 | },
75 | },
76 | {
77 | label: "Action",
78 | identifier: "action",
79 | type: "select",
80 | options: [
81 | {
82 | value: "purchase",
83 | title: "Purchase",
84 | },
85 | {
86 | value: "verify",
87 | title: "Verify",
88 | },
89 | {
90 | value: "refund",
91 | title: "Refund",
92 | },
93 | ],
94 | required: false,
95 | readOnly: false,
96 | quickAdd: true,
97 | unique: false,
98 | hidden: false,
99 | tooltip: "Action can be purchase, verify or refund.",
100 | multipleValues: false,
101 | defaultValue: "purchase",
102 | },
103 | {
104 | label: "Payment Method",
105 | identifier: "paymentMethod",
106 | type: "select",
107 | options: [
108 | {
109 | value: "cash",
110 | title: "Cash",
111 | },
112 | {
113 | value: "credit_card",
114 | title: "Credit Card",
115 | },
116 | {
117 | value: "debit_card",
118 | title: "Debit Card",
119 | },
120 | {
121 | value: "online",
122 | title: "Online / Netbanking",
123 | },
124 | ],
125 | required: false,
126 | readOnly: false,
127 | quickAdd: true,
128 | unique: false,
129 | hidden: false,
130 | tooltip: "Method of Payment.",
131 | multipleValues: false,
132 | defaultValue: "cash",
133 | },
134 | ],
135 | },
136 | ];
137 |
138 | function TransactionFormDrawer(props) {
139 | const { title, onSave, showMore, open } = props;
140 |
141 | const values = props.transaction || extractValues(groups);
142 | return (
143 |
151 | );
152 | }
153 |
154 | TransactionFormDrawer.propTypes = {
155 | title: PropTypes.string.isRequired,
156 | showMore: PropTypes.bool,
157 | transaction: PropTypes.object,
158 | onSave: PropTypes.func.isRequired,
159 | };
160 |
161 | TransactionFormDrawer.defaultProps = {
162 | showMore: false,
163 | transaction: null,
164 | onCancel: null,
165 | };
166 |
167 | export default TransactionFormDrawer;
168 |
--------------------------------------------------------------------------------
/src/workspace/analytics/Analytics.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import Grid from "@material-ui/core/Grid";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import { connect } from "react-redux";
5 | import * as actions from "../../redux/actions";
6 | import WorkspaceToolbar from "../common/WorkspaceToolbar";
7 | import NoRecords from "../common/NoRecords";
8 | import Typography from "@material-ui/core/Typography";
9 | import SubscriptionsSummary from "./SubscriptionsSummary";
10 | import SubscriberCharts from "./SubscriberCharts";
11 | import RevenueSummary from "./RevenueSummary";
12 | import RevenueCharts from "./RevenueCharts";
13 | import PlanSummary from "./PlanSummary";
14 | import PlanCharts from "./PlanCharts";
15 |
16 | const useStyles = makeStyles((theme) => ({
17 | item: {
18 | padding: 8,
19 | },
20 | title: {
21 | margin: 24,
22 | marginBottom: 0,
23 | },
24 | }));
25 |
26 | function Analytics(props) {
27 | const classes = useStyles();
28 | const { analytics, fetchAnalytics } = props;
29 |
30 | useEffect(() => {
31 | fetchAnalytics();
32 | }, [fetchAnalytics]);
33 |
34 | if (analytics) {
35 | const subscriptionSummary = analytics.subscriptionSummary;
36 | const revenueSummary = analytics.revenueSummary;
37 | const planSummary = analytics.planSummary;
38 | const subscriberData = analytics.subscriberData;
39 | const churnRateData = analytics.churnRateData;
40 | const revenueData = analytics.revenueData;
41 | const transactionData = analytics.transactionData;
42 | const planData = analytics.planData;
43 | const conversionData = analytics.conversionData;
44 |
45 | return (
46 |
47 |
48 |
49 |
54 | Subscribers
55 |
56 |
57 |
63 |
64 |
65 |
71 |
75 |
76 |
77 |
78 |
79 |
84 | Revenue
85 |
86 |
87 |
93 |
94 |
95 |
101 |
105 |
106 |
107 |
108 |
109 |
114 | Plans
115 |
116 |
117 |
123 |
124 |
125 |
131 |
135 |
136 |
137 |
138 |
139 | );
140 | } else {
141 | return (
142 |
146 | );
147 | }
148 | }
149 |
150 | function mapStateToProps(props) {
151 | return {
152 | analytics: props.analytics,
153 | };
154 | }
155 |
156 | const mapDispatchToProps = {
157 | fetchAnalytics: actions.fetchAnalytics,
158 | };
159 |
160 | export default connect(mapStateToProps, mapDispatchToProps)(Analytics);
161 |
--------------------------------------------------------------------------------
/src/workspace/account/AccountCard.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import clsx from "clsx";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import Card from "@material-ui/core/Card";
5 | import CardHeader from "@material-ui/core/CardHeader";
6 | import CardContent from "@material-ui/core/CardContent";
7 | import Avatar from "@material-ui/core/Avatar";
8 | import Grid from "@material-ui/core/Grid";
9 | import Typography from "@material-ui/core/Typography";
10 | import { green } from "@material-ui/core/colors";
11 | import Button from "@material-ui/core/Button";
12 | import CardActions from "@material-ui/core/CardActions";
13 |
14 | import EditIcon from "@material-ui/icons/Edit";
15 | // import CloseIcon from "@material-ui/icons/Close";
16 |
17 | import { findCountryByCode } from "../../common/countries";
18 |
19 | const useStyles = makeStyles((theme) => ({
20 | root: {},
21 | avatar: {
22 | backgroundColor: green[500],
23 | },
24 | title: {
25 | fontWeight: 600,
26 | fontSize: 14,
27 | },
28 | value: {
29 | fontSize: 15,
30 | },
31 | icon: {
32 | display: "inline-block",
33 | marginRight: 4,
34 | },
35 | close: {
36 | marginLeft: "auto",
37 | color: theme.palette.error.main,
38 | paddingLeft: 16,
39 | paddingRight: 16,
40 | },
41 | edit: {
42 | marginLeft: "auto",
43 | color: theme.palette.primary.main,
44 | paddingLeft: 16,
45 | paddingRight: 16,
46 | },
47 | }));
48 |
49 | function getCountryName(code) {
50 | if (!code) {
51 | return null;
52 | }
53 | const country = findCountryByCode(code);
54 | console.log(country);
55 | return country ? country.label : null;
56 | }
57 |
58 | const fields = [
59 | {
60 | identifier: "emailAddress",
61 | title: "Email Address",
62 | size: 6,
63 | render: (props) =>
64 | props.emailAddress ? props.emailAddress : "Unavailable",
65 | },
66 | {
67 | identifier: "phoneNumber",
68 | title: "Phone Number",
69 | size: 6,
70 | render: (props) =>
71 | props.phoneNumber ? props.phoneNumber : "Unavailable",
72 | },
73 | {
74 | identifier: "address",
75 | title: "Address",
76 | size: 6,
77 | render: (props) => {
78 | const {
79 | addressLine1,
80 | addressLine2,
81 | city,
82 | state,
83 | zipCode,
84 | country,
85 | } = props;
86 | const available =
87 | addressLine1 ||
88 | addressLine2 ||
89 | city ||
90 | state ||
91 | zipCode ||
92 | country;
93 | return available ? (
94 |
95 | {addressLine1 && (
96 |
97 | {addressLine1}
98 |
99 | )}
100 | {addressLine2 && (
101 |
102 | {addressLine2}
103 |
104 |
105 | )}
106 | {city && (
107 |
108 | {city}
109 |
110 |
111 | )}
112 | {state && (
113 |
114 | {state}
115 |
116 |
117 | )}
118 | {country && (
119 |
120 | {getCountryName(country)}
121 |
122 |
123 | )}
124 |
125 | ) : (
126 | "Unavailable"
127 | );
128 | },
129 | },
130 | ];
131 |
132 | function AccountCard(props) {
133 | const classes = useStyles();
134 | const {
135 | className,
136 | firstName,
137 | lastName,
138 | userName,
139 | onEdit /*, onClose */,
140 | } = props;
141 |
142 | return (
143 |
144 |
147 | {firstName.substring(0, 1).toUpperCase()}
148 |
149 | }
150 | title={firstName + " " + lastName}
151 | subheader={userName}
152 | />
153 |
154 |
155 |
156 | {fields.map((field) => (
157 |
158 |
159 |
164 | {field.title}
165 |
166 |
167 | {field.render(props)}
168 |
169 |
170 |
171 | ))}
172 |
173 |
174 |
175 |
176 | {/*
177 |
178 | Close
179 | */}
180 |
181 |
182 | Edit
183 |
184 |
185 |
186 | );
187 | }
188 |
189 | export default AccountCard;
190 |
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === "localhost" ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === "[::1]" ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener("load", () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | "This web app is being served cache-first by a service " +
46 | "worker. To learn more, visit https://bit.ly/CRA-PWA"
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then((registration) => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === "installed") {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | "New content is available and will be used when all " +
74 | "tabs for this page are closed. See https://bit.ly/CRA-PWA."
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log("Content is cached for offline use.");
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch((error) => {
97 | console.error("Error during service worker registration:", error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { "Service-Worker": "script" },
105 | })
106 | .then((response) => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get("content-type");
109 | if (
110 | response.status === 404 ||
111 | (contentType != null &&
112 | contentType.indexOf("javascript") === -1)
113 | ) {
114 | // No service worker found. Probably a different app. Reload the page.
115 | navigator.serviceWorker.ready.then((registration) => {
116 | registration.unregister().then(() => {
117 | window.location.reload();
118 | });
119 | });
120 | } else {
121 | // Service worker found. Proceed as normal.
122 | registerValidSW(swUrl, config);
123 | }
124 | })
125 | .catch(() => {
126 | console.log(
127 | "No internet connection found. App is running in offline mode."
128 | );
129 | });
130 | }
131 |
132 | export function unregister() {
133 | if ("serviceWorker" in navigator) {
134 | navigator.serviceWorker.ready
135 | .then((registration) => {
136 | registration.unregister();
137 | })
138 | .catch((error) => {
139 | console.error(error.message);
140 | });
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src/layouts/MainDrawer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import clsx from "clsx";
3 | import { makeStyles, useTheme } from "@material-ui/core/styles";
4 | import { withRouter } from "react-router-dom";
5 | import Drawer from "@material-ui/core/Drawer";
6 | import List from "@material-ui/core/List";
7 | import Divider from "@material-ui/core/Divider";
8 | import IconButton from "@material-ui/core/IconButton";
9 | import ListItem from "@material-ui/core/ListItem";
10 | import ListItemIcon from "@material-ui/core/ListItemIcon";
11 | import ListItemText from "@material-ui/core/ListItemText";
12 |
13 | import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
14 | import ChevronRightIcon from "@material-ui/icons/ChevronRight";
15 | import AccountsIcon from "@material-ui/icons/AccountCircle";
16 | import PreferencesIcon from "@material-ui/icons/Settings";
17 | import AnalyticsIcon from "@material-ui/icons/BarChart";
18 | import TransactionsIcon from "@material-ui/icons/MonetizationOn";
19 | import SubscriptionsIcon from "@material-ui/icons/Autorenew";
20 | import IvoicesIcon from "@material-ui/icons/Receipt";
21 | import PlansIcon from "@material-ui/icons/LocalOffer";
22 | import APIKeysIcon from "@material-ui/icons/Code";
23 | import WebhooksIcon from "@material-ui/icons/CallReceived";
24 |
25 | const drawerWidth = 240;
26 |
27 | const useStyles = makeStyles((theme) => ({
28 | drawer: {
29 | width: drawerWidth,
30 | flexShrink: 0,
31 | whiteSpace: "nowrap",
32 | },
33 | drawerOpen: {
34 | width: drawerWidth,
35 | transition: theme.transitions.create("width", {
36 | easing: theme.transitions.easing.sharp,
37 | duration: theme.transitions.duration.enteringScreen,
38 | }),
39 | },
40 | drawerClose: {
41 | transition: theme.transitions.create("width", {
42 | easing: theme.transitions.easing.sharp,
43 | duration: theme.transitions.duration.leavingScreen,
44 | }),
45 | overflowX: "hidden",
46 | width: 60,
47 | },
48 | toolbar: {
49 | display: "flex",
50 | alignItems: "center",
51 | justifyContent: "flex-end",
52 | padding: theme.spacing(0, 1),
53 | // necessary for content to be below app bar
54 | ...theme.mixins.toolbar,
55 | },
56 | }));
57 |
58 | const navigationGroups = [
59 | {
60 | id: "primary",
61 | items: [
62 | {
63 | id: "accounts",
64 | title: "Accounts",
65 | icon: ,
66 | link: "/accounts",
67 | },
68 | {
69 | id: "subscriptions",
70 | title: "Subscriptions",
71 | icon: ,
72 | link: "/subscriptions",
73 | },
74 | {
75 | id: "invoices",
76 | title: "Invoices",
77 | icon: ,
78 | link: "/invoices",
79 | },
80 | {
81 | id: "transactions",
82 | title: "Transactions",
83 | icon: ,
84 | link: "/transactions",
85 | },
86 | ],
87 | },
88 | {
89 | id: "dashboard",
90 | items: [
91 | {
92 | id: "analytics",
93 | title: "Analytics",
94 | icon: ,
95 | link: "/analytics",
96 | },
97 | ],
98 | },
99 | {
100 | id: "developers",
101 | items: [
102 | {
103 | id: "api-keys",
104 | title: "API Keys",
105 | icon: ,
106 | link: "/api-keys",
107 | },
108 | {
109 | id: "webhooks",
110 | title: "Webhooks",
111 | icon: ,
112 | link: "/webhooks",
113 | },
114 | ],
115 | },
116 | {
117 | id: "configuration",
118 | items: [
119 | {
120 | id: "plans",
121 | title: "Plans",
122 | icon: ,
123 | link: "/plans",
124 | },
125 | {
126 | id: "preferences",
127 | title: "Preferences",
128 | icon: ,
129 | link: "/preferences",
130 | },
131 | ],
132 | },
133 | ];
134 |
135 | function MainDrawer(props) {
136 | const makeLinkHandler = (url) => () => props.history.push(url);
137 |
138 | const classes = useStyles();
139 | const theme = useTheme();
140 | const { open, handleCloseDrawer } = props;
141 | return (
142 |
155 |
156 |
157 | {theme.direction === "rtl" ? (
158 |
159 | ) : (
160 |
161 | )}
162 |
163 |
164 |
165 | {navigationGroups.map((group, index) => (
166 |
167 |
168 | {group.items.map((item, index) => (
169 |
174 | {item.icon}
175 |
176 |
177 | ))}
178 |
179 | {index + 1 < navigationGroups.length && }
180 |
181 | ))}
182 |
183 | );
184 | }
185 |
186 | export default withRouter(MainDrawer);
187 |
--------------------------------------------------------------------------------
/src/workspace/account/ViewAccount.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | // import Grid from "@material-ui/core/Grid";
3 | import CircularProgress from "@material-ui/core/CircularProgress";
4 | import Typography from "@material-ui/core/Typography";
5 | import { makeStyles } from "@material-ui/core/styles";
6 | import { connect } from "react-redux";
7 | import { useParams, withRouter } from "react-router-dom";
8 |
9 | import WorkspaceToolbar from "../common/WorkspaceToolbar";
10 | // import SubscriptionCard from "../subscription/SubscriptionCard";
11 | import AccountCard from "./AccountCard";
12 | import * as actions from "../../redux/actions";
13 |
14 | const useStyles = makeStyles((theme) => ({
15 | container: {
16 | padding: 16,
17 | },
18 | accountCard: {
19 | maxWidth: 600,
20 | },
21 | section: {
22 | marginBottom: 32,
23 | },
24 | sectionTitle: {
25 | fontSize: 20,
26 | marginBottom: 16,
27 | fontWeight: 500,
28 | },
29 | subscriptions: {},
30 | progress: {
31 | position: "fixed",
32 | top: "50%",
33 | left: "50%",
34 | marginTop: -24,
35 | marginLeft: -24,
36 | },
37 | }));
38 |
39 | /*
40 | const subscriptions = [
41 | {
42 | identifier: "1",
43 | currentPeriod: "May 03 1999 — May 19 1999",
44 | plan: "Premium",
45 | status: "trial",
46 | termBehavior: "Renews",
47 | collection: "Manual",
48 | renewsOn: "May 03 1999, 3:36 AM",
49 | startedOn: "May 19 1999, 3:36 AM",
50 | pricePerUnit: "100 INR",
51 | estimatedTotal: "200 INR",
52 | },
53 | {
54 | identifier: "2",
55 | currentPeriod: "May 03 1999 — May 19 1999",
56 | plan: "Platinum",
57 | status: "active",
58 | termBehavior: "Renews",
59 | collection: "Manual",
60 | renewsOn: "May 03 1999, 3:36 AM",
61 | startedOn: "May 19 1999, 3:36 AM",
62 | pricePerUnit: "100 INR",
63 | estimatedTotal: "200 INR",
64 | },
65 | ];
66 | */
67 |
68 | // Some fields aren't being rendered.
69 | function ViewAccount(props) {
70 | const classes = useStyles();
71 | const { fetchAccount, clearAccount, account, editAccount } = props;
72 | const { identifier } = useParams();
73 |
74 | const handleEditAccount = () => {
75 | editAccount(account);
76 | };
77 |
78 | useEffect(() => {
79 | fetchAccount(identifier);
80 | return clearAccount;
81 | }, [identifier, fetchAccount, clearAccount]);
82 |
83 | return (
84 |
85 |
86 | {!account && (
87 |
88 | )}
89 | {account && (
90 |
91 |
92 |
96 | General
97 |
98 |
113 |
114 |
115 | {/*
116 |
120 | Subscriptions
121 |
122 |
127 | {subscriptions.map((subscription) => (
128 |
133 |
148 |
149 | ))}
150 |
151 |
*/}
152 |
153 | )}
154 |
155 | );
156 | }
157 |
158 | function mapStateToProps(state) {
159 | return {
160 | account: state.account,
161 | };
162 | }
163 |
164 | const mapDispatchToProps = {
165 | fetchAccount: actions.fetchAccount,
166 | clearAccount: actions.clearAccount,
167 | editAccount: actions.editAccount,
168 | };
169 |
170 | export default connect(
171 | mapStateToProps,
172 | mapDispatchToProps
173 | )(withRouter(ViewAccount));
174 |
--------------------------------------------------------------------------------
/src/redux/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from "redux";
2 | import * as ActionTypes from "./actionTypes";
3 |
4 | function dialogReducer(state = null, action) {
5 | switch (action.type) {
6 | case ActionTypes.NEW_API_KEY:
7 | case ActionTypes.NEW_ACCOUNT:
8 | case ActionTypes.NEW_SUBSCRIPTION:
9 | case ActionTypes.NEW_INVOICE:
10 | case ActionTypes.NEW_TRANSACTION:
11 | case ActionTypes.NEW_PLAN:
12 | case ActionTypes.EDIT_ACCOUNT:
13 | case ActionTypes.EDIT_TRANSACTION:
14 | case ActionTypes.EDIT_PLAN:
15 | case ActionTypes.EDIT_INVOICE: {
16 | return action.type;
17 | }
18 |
19 | case ActionTypes.CLOSE_DIALOG: {
20 | return null;
21 | }
22 |
23 | default: {
24 | return state;
25 | }
26 | }
27 | }
28 |
29 | function notificationReducer(state = null, action) {
30 | if (action.type === "SHOW_NOTIFICATION") {
31 | return {
32 | message: action.payload.message,
33 | category: action.payload.category,
34 | };
35 | } else if (action.type === "CLOSE_NOTIFICATION") {
36 | return null;
37 | }
38 | return state;
39 | }
40 |
41 | function analyticsReducer(state = null, action) {
42 | switch (action.type) {
43 | case ActionTypes.FETCH_ANALYTICS_COMPLETE: {
44 | return action.payload;
45 | }
46 |
47 | default: {
48 | return state;
49 | }
50 | }
51 | }
52 |
53 | function accountsReducer(state = null, action) {
54 | switch (action.type) {
55 | case ActionTypes.FETCH_ACCOUNTS_COMPLETE: {
56 | return action.payload;
57 | }
58 |
59 | default: {
60 | return state;
61 | }
62 | }
63 | }
64 |
65 | function accountReducer(state = null, action) {
66 | switch (action.type) {
67 | case ActionTypes.FETCH_ACCOUNT_COMPLETE:
68 | case ActionTypes.EDIT_ACCOUNT: {
69 | return action.payload;
70 | }
71 |
72 | case ActionTypes.CLEAR_ACCOUNT: {
73 | return null;
74 | }
75 |
76 | default: {
77 | return state;
78 | }
79 | }
80 | }
81 |
82 | function subscriptionsReducer(state = null, action) {
83 | switch (action.type) {
84 | case ActionTypes.FETCH_SUBSCRIPTIONS_COMPLETE: {
85 | return action.payload;
86 | }
87 |
88 | default: {
89 | return state;
90 | }
91 | }
92 | }
93 |
94 | function subscriptionReducer(state = null, action) {
95 | switch (action.type) {
96 | case ActionTypes.FETCH_SUBSCRIPTION_COMPLETE: {
97 | return action.payload;
98 | }
99 |
100 | case ActionTypes.CLEAR_SUBSCRIPTION: {
101 | return null;
102 | }
103 |
104 | default: {
105 | return state;
106 | }
107 | }
108 | }
109 |
110 | function invoicesReducer(state = null, action) {
111 | switch (action.type) {
112 | case ActionTypes.FETCH_INVOICES_COMPLETE: {
113 | return action.payload;
114 | }
115 |
116 | default: {
117 | return state;
118 | }
119 | }
120 | }
121 |
122 | function invoiceReducer(state = null, action) {
123 | switch (action.type) {
124 | case ActionTypes.FETCH_INVOICE_COMPLETE:
125 | case ActionTypes.EDIT_INVOICE: {
126 | return action.payload;
127 | }
128 |
129 | case ActionTypes.CLEAR_INVOICE: {
130 | return null;
131 | }
132 |
133 | default: {
134 | return state;
135 | }
136 | }
137 | }
138 |
139 | function transactionsReducer(state = null, action) {
140 | switch (action.type) {
141 | case ActionTypes.FETCH_TRANSACTIONS_COMPLETE: {
142 | return action.payload;
143 | }
144 |
145 | default: {
146 | return state;
147 | }
148 | }
149 | }
150 |
151 | function transactionReducer(state = null, action) {
152 | switch (action.type) {
153 | case ActionTypes.FETCH_TRANSACTION_COMPLETE:
154 | case ActionTypes.EDIT_TRANSACTION: {
155 | return action.payload;
156 | }
157 |
158 | case ActionTypes.CLEAR_TRANSACTION: {
159 | return null;
160 | }
161 |
162 | default: {
163 | return state;
164 | }
165 | }
166 | }
167 |
168 | function plansReducer(state = null, action) {
169 | switch (action.type) {
170 | case ActionTypes.FETCH_PLANS_COMPLETE: {
171 | return action.payload;
172 | }
173 |
174 | default: {
175 | return state;
176 | }
177 | }
178 | }
179 |
180 | function planReducer(state = null, action) {
181 | switch (action.type) {
182 | case ActionTypes.FETCH_PLAN_COMPLETE:
183 | case ActionTypes.EDIT_PLAN: {
184 | return action.payload;
185 | }
186 |
187 | case ActionTypes.CLEAR_ACCOUNT: {
188 | return null;
189 | }
190 |
191 | default: {
192 | return state;
193 | }
194 | }
195 | }
196 |
197 | function userReducer(state = null, action) {
198 | switch (action.type) {
199 | case ActionTypes.FETCH_USER_COMPLETE: {
200 | return action.payload;
201 | }
202 |
203 | default: {
204 | return state;
205 | }
206 | }
207 | }
208 |
209 | function isUserLoadingReducer(state = true, action) {
210 | switch (action.type) {
211 | case ActionTypes.FETCH_USER_COMPLETE:
212 | case ActionTypes.FETCH_USER_FAILED: {
213 | return false;
214 | }
215 |
216 | default: {
217 | return state;
218 | }
219 | }
220 | }
221 |
222 | function internalRedirectReducer(state = null, action) {
223 | switch (action.type) {
224 | case ActionTypes.INTERNAL_REDIRECT: {
225 | return action.payload;
226 | }
227 |
228 | default: {
229 | return state;
230 | }
231 | }
232 | }
233 |
234 | const rootReducer = combineReducers({
235 | openDialog: dialogReducer,
236 | notification: notificationReducer,
237 | analytics: analyticsReducer,
238 | accounts: accountsReducer,
239 | account: accountReducer,
240 | subscriptions: subscriptionsReducer,
241 | subscription: subscriptionReducer,
242 | transactions: transactionsReducer,
243 | transaction: transactionReducer,
244 | plans: plansReducer,
245 | plan: planReducer,
246 | invoices: invoicesReducer,
247 | invoice: invoiceReducer,
248 | user: userReducer,
249 | isUserLoading: isUserLoadingReducer,
250 | internalRedirect: internalRedirectReducer,
251 | });
252 |
253 | export default rootReducer;
254 |
--------------------------------------------------------------------------------
/src/workspace/subscription/SubscriptionFormDrawer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { extractValues } from "../common/RecordForm";
5 | import FormDrawer from "../common/FormDrawer";
6 |
7 | const groups = [
8 | {
9 | label: "Basic",
10 | children: [
11 | {
12 | label: "Account",
13 | identifier: "accountId",
14 | type: "account_lookup",
15 | required: true,
16 | readOnly: false,
17 | quickAdd: true,
18 | unique: false,
19 | hidden: false,
20 | tooltip: "The account associated with the subscription.",
21 | multipleValues: false,
22 | defaultValue: null,
23 | },
24 | {
25 | label: "Plan",
26 | identifier: "planId",
27 | type: "plan_lookup",
28 | required: true,
29 | readOnly: false,
30 | quickAdd: true,
31 | unique: false,
32 | hidden: false,
33 | tooltip: "The plan associated with the subscription.",
34 | multipleValues: false,
35 | defaultValue: null,
36 | },
37 | {
38 | label: "Price Per Billing Cycle",
39 | identifier: "pricePerBillingCycle",
40 | type: "number",
41 | required: true,
42 | readOnly: false,
43 | quickAdd: true,
44 | unique: false,
45 | hidden: false,
46 | tooltip: "The price per billing cycle.",
47 | multipleValues: true,
48 | defaultValue: 0,
49 | validations: "isNumeric",
50 | validationErrors: {
51 | isNumeric: "Please enter a valid number.",
52 | },
53 | },
54 | {
55 | label: "Setup Fee",
56 | identifier: "setupFee",
57 | type: "number",
58 | required: true,
59 | readOnly: false,
60 | quickAdd: true,
61 | unique: false,
62 | hidden: false,
63 | tooltip: "The fee required for setup of the subscription.",
64 | multipleValues: true,
65 | defaultValue: 0,
66 | validations: "isNumeric",
67 | validationErrors: {
68 | isNumeric: "Please enter a valid number.",
69 | },
70 | },
71 | {
72 | label: "Quantity",
73 | identifier: "quantity",
74 | type: "number",
75 | required: true,
76 | readOnly: false,
77 | quickAdd: true,
78 | unique: false,
79 | hidden: false,
80 | tooltip: "The quantity of the plan.",
81 | multipleValues: false,
82 | defaultValue: null,
83 | validations: "isNumeric",
84 | validationErrors: {
85 | isNumeric: "Please enter a valid number.",
86 | },
87 | },
88 | {
89 | label: "Starts",
90 | identifier: "startsAt",
91 | type: "date_time",
92 | required: true,
93 | readOnly: false,
94 | quickAdd: true,
95 | unique: false,
96 | hidden: false,
97 | tooltip: "Start date time of the subscription.",
98 | multipleValues: false,
99 | defaultValue: new Date(),
100 | },
101 | {
102 | label: "Total Billing Cycles",
103 | identifier: "totalBillingCycles",
104 | type: "number",
105 | required: true,
106 | readOnly: false,
107 | quickAdd: true,
108 | unique: false,
109 | hidden: false,
110 | tooltip: "The total number of billing cycles in the term.",
111 | multipleValues: false,
112 | defaultValue: 0,
113 | validations: "isInt",
114 | validationErrors: {
115 | isInt: "Please enter a valid integer.",
116 | },
117 | },
118 | {
119 | label: "Renews",
120 | identifier: "renews",
121 | type: "switch",
122 | required: false,
123 | readOnly: false,
124 | quickAdd: true,
125 | unique: false,
126 | hidden: false,
127 | tooltip:
128 | "Boolean value stating whether subscription is recurring.",
129 | multipleValues: false,
130 | defaultValue: false,
131 | },
132 | {
133 | label: "Notes",
134 | identifier: "notes",
135 | type: "large_text",
136 | required: false,
137 | readOnly: false,
138 | quickAdd: true,
139 | unique: false,
140 | hidden: false,
141 | rows: 4,
142 | tooltip:
143 | "Notes about this subscription. It is displayed in the invoice.",
144 | defaultValue: "",
145 | validations: {
146 | maxLength: 200,
147 | },
148 | validationErrors: {
149 | maxLength: "The notes must be 0-200 characters long.",
150 | },
151 | },
152 | {
153 | label: "Terms and Conditions",
154 | identifier: "termsAndConditions",
155 | type: "large_text",
156 | required: false,
157 | readOnly: false,
158 | quickAdd: true,
159 | unique: false,
160 | hidden: false,
161 | rows: 4,
162 | tooltip:
163 | "Terms and conditions applied to this subscription. It is displayed in the invoice and can be used to highlight important terms and conditions related to the subscription or charges.",
164 | defaultValue: "",
165 | validations: {
166 | maxLength: 200,
167 | },
168 | validationErrors: {
169 | maxLength:
170 | "The terms and conditions must be 0-200 characters long.",
171 | },
172 | },
173 | ],
174 | },
175 | ];
176 |
177 | function SubscriptionFormDrawer(props) {
178 | const { title, onSave, showMore, open } = props;
179 |
180 | const values = props.account || extractValues(groups);
181 | return (
182 | {
188 | console.log(values);
189 | onSave(values);
190 | }}
191 | open={open}
192 | />
193 | );
194 | }
195 |
196 | SubscriptionFormDrawer.propTypes = {
197 | title: PropTypes.string.isRequired,
198 | showMore: PropTypes.bool,
199 | account: PropTypes.object,
200 | onSave: PropTypes.func.isRequired,
201 | };
202 |
203 | SubscriptionFormDrawer.defaultProps = {
204 | showMore: false,
205 | account: null,
206 | onCancel: null,
207 | };
208 |
209 | export default SubscriptionFormDrawer;
210 |
--------------------------------------------------------------------------------
/src/server/api.js:
--------------------------------------------------------------------------------
1 | import axios from "axios";
2 |
3 | const DEFAULT_API_VERSION = "v1";
4 |
5 | function initializeAccount(account) {
6 | account.createdAt = new Date(account.createdAt);
7 | account.updatedAt = new Date(account.updatedAt);
8 | return account;
9 | }
10 |
11 | function initializeSubscription(subscription) {
12 | const {
13 | startsAt,
14 | activatedAt,
15 | cancelledAt,
16 | pausedAt,
17 | currentPeriodStart,
18 | currentPeriodEnd,
19 | createdAt,
20 | updatedAt,
21 | } = subscription;
22 |
23 | subscription.startsAt = new Date(startsAt);
24 | subscription.activatedAt = activatedAt ? new Date(activatedAt) : null;
25 | subscription.cancelledAt = cancelledAt ? new Date(cancelledAt) : null;
26 | subscription.pausedAt = pausedAt ? new Date(pausedAt) : null;
27 | subscription.currentPeriodStart = currentPeriodStart
28 | ? new Date(currentPeriodStart)
29 | : null;
30 | subscription.currentPeriodEnd = currentPeriodEnd
31 | ? new Date(currentPeriodEnd)
32 | : null;
33 | subscription.createdAt = new Date(createdAt);
34 | subscription.updatedAt = new Date(updatedAt);
35 |
36 | return subscription;
37 | }
38 |
39 | function initializeInvoice(invoice) {
40 | const { closedAt, dueAt, updatedAt, createdAt, items } = invoice;
41 | invoice.closedAt = closedAt ? new Date(closedAt) : null;
42 | invoice.dueAt = new Date(dueAt);
43 | invoice.updatedAt = new Date(updatedAt);
44 | invoice.createdAt = new Date(createdAt);
45 |
46 | for (let i = 0; i < items.length; i++) {
47 | const item = items[i];
48 | item.startedAt = new Date(item.startedAt);
49 | item.endedAt = new Date(item.endedAt);
50 | }
51 |
52 | return invoice;
53 | }
54 |
55 | function initializeTransaction(transaction) {
56 | transaction.createdAt = new Date(transaction.createdAt);
57 | transaction.updatedAt = new Date(transaction.updatedAt);
58 |
59 | return transaction;
60 | }
61 |
62 | function initializePlan(plan) {
63 | plan.createdAt = new Date(plan.createdAt);
64 | plan.updatedAt = new Date(plan.updatedAt);
65 |
66 | return plan;
67 | }
68 |
69 | export function newClient(version = DEFAULT_API_VERSION) {
70 | axios.defaults.baseURL = `${process.env.REACT_APP_API_URL}/api/${version}`;
71 |
72 | return {
73 | // Account
74 |
75 | newAccount: async (account) => {
76 | const response = await axios.post(`/accounts`, account);
77 | return initializeAccount(response.data);
78 | },
79 |
80 | saveAccount: async (account) => {
81 | const response = await axios.put(
82 | `/accounts/${account.id}`,
83 | account
84 | );
85 | return initializeAccount(response.data);
86 | },
87 |
88 | getAccount: async (id) => {
89 | const response = await axios.get(`/accounts/${id}`);
90 | return initializeAccount(response.data);
91 | },
92 |
93 | getAccounts: async (params) => {
94 | const response = await axios.get(`/accounts`, { params });
95 | const accounts = response.data;
96 | const { records } = accounts;
97 | for (let i = 0; i < records.length; i++) {
98 | records[i] = initializeAccount(records[i]);
99 | }
100 | return accounts;
101 | },
102 |
103 | // Subscription
104 | newSubscription: async (subscription) => {
105 | const response = await axios.post(`/subscriptions`, subscription);
106 | return initializeSubscription(response.data);
107 | },
108 |
109 | saveSubscription: async (subscription) => {
110 | const response = await axios.put(
111 | `/subscriptions/${subscription.id}`,
112 | subscription
113 | );
114 | return initializeSubscription(response.data);
115 | },
116 |
117 | getSubscription: async (id) => {
118 | const response = await axios.get(`/subscriptions/${id}`);
119 | return initializeSubscription(response.data);
120 | },
121 |
122 | getSubscriptions: async (params) => {
123 | const response = await axios.get(`/subscriptions`, { params });
124 | const subscriptions = response.data;
125 | const { records } = subscriptions;
126 | for (let i = 0; i < records.length; i++) {
127 | records[i] = initializeSubscription(records[i]);
128 | }
129 | return subscriptions;
130 | },
131 |
132 | // Invoice
133 | saveInvoice: async (invoice) => {
134 | const response = await axios.put(
135 | `/invoices/${invoice.id}`,
136 | invoice
137 | );
138 | return initializeInvoice(response.data);
139 | },
140 |
141 | getInvoice: async (id) => {
142 | const response = await axios.get(`/invoices/${id}`);
143 | return initializeInvoice(response.data);
144 | },
145 |
146 | getInvoices: async (params) => {
147 | const response = await axios.get(`/invoices`, { params });
148 | const invoices = response.data;
149 | const { records } = invoices;
150 | for (let i = 0; i < records.length; i++) {
151 | records[i] = initializeInvoice(records[i]);
152 | }
153 | return invoices;
154 | },
155 |
156 | // Transaction
157 | newTransaction: async (transaction) => {
158 | const response = await axios.post(`/transactions`, transaction);
159 | return initializeTransaction(response.data);
160 | },
161 |
162 | saveTransaction: async (transaction) => {
163 | const response = await axios.put(
164 | `/transactions/${transaction.id}`,
165 | transaction
166 | );
167 | return initializeTransaction(response.data);
168 | },
169 |
170 | getTransaction: async (id) => {
171 | const response = await axios.get(`/transactions/${id}`);
172 | return initializeTransaction(response.data);
173 | },
174 |
175 | getTransactions: async (params) => {
176 | const response = await axios.get(`/transactions`, { params });
177 | const transactions = response.data;
178 | const { records } = transactions;
179 | for (let i = 0; i < records.length; i++) {
180 | records[i] = initializeTransaction(records[i]);
181 | }
182 | return transactions;
183 | },
184 |
185 | // Plan
186 |
187 | newPlan: async (plan) => {
188 | const response = await axios.post(`/plans`, plan);
189 | return initializePlan(response);
190 | },
191 |
192 | savePlan: async (plan) => {
193 | const response = await axios.put(`/plans/${plan.id}`, plan);
194 | return initializePlan(response);
195 | },
196 |
197 | getPlan: async (id) => {
198 | const response = await axios.get(`/plans/${id}`);
199 | return initializePlan(response.data);
200 | },
201 |
202 | getPlans: async (params) => {
203 | const response = await axios.get(`/plans`, { params });
204 | const plans = response.data;
205 | const { records } = plans;
206 | for (let i = 0; i < records.length; i++) {
207 | records[i] = initializePlan(records[i]);
208 | }
209 | return plans;
210 | },
211 |
212 | // Analytics
213 | getAnalytics: (params) => axios.get(`/analytics`, { params }),
214 | };
215 | }
216 |
--------------------------------------------------------------------------------
/src/layouts/MainLayout.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from "react";
2 | import clsx from "clsx";
3 | import { makeStyles } from "@material-ui/core/styles";
4 | import Snackbar from "@material-ui/core/Snackbar";
5 | import SnackbarContent from "@material-ui/core/SnackbarContent";
6 |
7 | // TODO: Update this import when Material UI moves this to production.
8 | import MuiAlert from "@material-ui/lab/Alert";
9 | import CircularProgress from "@material-ui/core/CircularProgress";
10 | import { renderRoutes } from "react-router-config";
11 | import MainToolbar from "./MainToolbar";
12 | import MainDrawer from "./MainDrawer";
13 | import { connect } from "react-redux";
14 |
15 | import routes from "../routes";
16 | import AccountFormDrawer from "../workspace/account/AccountFormDrawer";
17 | import EditAccount from "../workspace/account/EditAccount";
18 | import SubscriptionFormDrawer from "../workspace/subscription/SubscriptionFormDrawer";
19 | import TransactionFormDrawer from "../workspace/transaction/TransactionFormDrawer";
20 | import PlanFormDrawer from "../workspace/plan/PlanFormDrawer";
21 | import APIKeyFormDrawer from "../workspace/api-key/APIKeyFormDrawer";
22 | import EditPlan from "../workspace/plan/EditPlan";
23 | import EditInvoice from "../workspace/invoice/EditInvoice";
24 | import EditTransaction from "../workspace/transaction/EditTransaction";
25 | import * as actions from "../redux/actions";
26 |
27 | const miniDrawerWidth = 60;
28 | const drawerWidth = 240;
29 |
30 | function Alert(props) {
31 | return ;
32 | }
33 |
34 | const useStyles = makeStyles((theme) => ({
35 | root: {},
36 | content: {
37 | transition: theme.transitions.create("margin", {
38 | easing: theme.transitions.easing.easeOut,
39 | duration: theme.transitions.duration.enteringScreen,
40 | }),
41 | marginLeft: miniDrawerWidth,
42 | marginTop: 64,
43 | },
44 | contentShift: {
45 | marginLeft: drawerWidth,
46 | marginTop: 64,
47 | width: `calc(100% - ${drawerWidth}px)`,
48 | transition: theme.transitions.create(["width", "margin"], {
49 | easing: theme.transitions.easing.sharp,
50 | duration: theme.transitions.duration.enteringScreen,
51 | }),
52 | },
53 | progress: {
54 | maxWidth: 24,
55 | maxHeight: 24,
56 | color: "white",
57 | },
58 | drawer: {
59 | width: 50,
60 | },
61 | suspense: {
62 | position: "fixed",
63 | top: "50%",
64 | left: "50%",
65 | marginTop: -24,
66 | marginLeft: -24,
67 | },
68 | }));
69 |
70 | // TODO: The layouts should be configurable.
71 | // TODO: Show drawer instead of toolbar for smaller screens.
72 | function MainLayout(props) {
73 | const {
74 | openDialog,
75 | notification,
76 | closeNotification,
77 | createAccount,
78 | createSubscription,
79 | createTransaction,
80 | createPlan,
81 | } = props;
82 | const [drawerOpen, setDrawerOpen] = React.useState(false);
83 | const classes = useStyles();
84 |
85 | const toggleDrawer = () => {
86 | setDrawerOpen(!drawerOpen);
87 | };
88 |
89 | const handleCloseNotification = (event, reason) => {
90 | if (reason !== "clickaway") {
91 | closeNotification();
92 | }
93 | };
94 |
95 | const renderNotification = (notification) => {
96 | if (notification) {
97 | if (notification.category === "LOADING") {
98 | return (
99 |
104 |
110 | }
111 | />
112 |
113 | );
114 | }
115 |
116 | const severityMap = {
117 | SUCCESS: "success",
118 | ERROR: "error",
119 | };
120 | const severity = severityMap[notification.category]
121 | ? severityMap[notification.category]
122 | : "SUCCESS";
123 |
124 | return (
125 |
130 |
134 | {notification.message}
135 |
136 |
137 | );
138 | }
139 | return null;
140 | };
141 |
142 | return (
143 |
144 |
145 |
149 |
153 |
154 |
159 |
165 | }
166 | >
167 | {renderRoutes(routes)}
168 |
169 | {props.children}
170 |
171 |
172 | {/* */}
173 |
174 |
175 |
180 |
185 |
190 |
195 | {}}
198 | open={openDialog === "NEW_API_KEY"}
199 | />
200 |
201 | {openDialog === "EDIT_INVOICE" && }
202 | {openDialog === "EDIT_TRANSACTION" && }
203 | {openDialog === "EDIT_ACCOUNT" && }
204 | {openDialog === "EDIT_PLAN" && }
205 |
206 | {renderNotification(notification)}
207 |
208 | );
209 | }
210 |
211 | function mapStateToProps(state) {
212 | return {
213 | openDialog: state.openDialog,
214 | notification: state.notification,
215 | };
216 | }
217 |
218 | const mapDispatchToProps = {
219 | closeNotification: actions.closeNotification,
220 | createAccount: actions.createAccount,
221 | createPlan: actions.createPlan,
222 | createSubscription: actions.createSubscription,
223 | createTransaction: actions.createTransaction,
224 | };
225 |
226 | export default connect(mapStateToProps, mapDispatchToProps)(MainLayout);
227 |
--------------------------------------------------------------------------------
/src/workspace/common/WorkspaceTable.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { makeStyles } from "@material-ui/core/styles";
3 | import Table from "@material-ui/core/Table";
4 | import TableBody from "@material-ui/core/TableBody";
5 | import TableCell from "@material-ui/core/TableCell";
6 | import TableContainer from "@material-ui/core/TableContainer";
7 | import TablePagination from "@material-ui/core/TablePagination";
8 | import TableRow from "@material-ui/core/TableRow";
9 | import Paper from "@material-ui/core/Paper";
10 | // import Checkbox from "@material-ui/core/Checkbox";
11 |
12 | import WorkspaceTableHead from "./WorkspaceTableHead";
13 |
14 | /* The default comparator is recommended if the column IDs and the values are primitive.
15 | * Otherwise you should provide a custom descending comparator.
16 | */
17 | function defaultDescendingComparator(a, b, orderBy) {
18 | if (b[orderBy] < a[orderBy]) {
19 | return -1;
20 | }
21 | if (b[orderBy] > a[orderBy]) {
22 | return 1;
23 | }
24 | return 0;
25 | }
26 |
27 | function getComparator(descendingComparator, order, orderBy) {
28 | return order === "desc"
29 | ? (a, b) => descendingComparator(a, b, orderBy)
30 | : (a, b) => -descendingComparator(a, b, orderBy);
31 | }
32 |
33 | function stableSort(array, comparator) {
34 | const auxillary = array.map((value, index) => [value, index]);
35 | auxillary.sort((a, b) => {
36 | const order = comparator(a[0], b[0]);
37 | if (order !== 0) {
38 | return order;
39 | }
40 | return a[1] - b[1];
41 | });
42 | return auxillary.map((value) => value[0]);
43 | }
44 |
45 | const useStyles = makeStyles((theme) => ({
46 | root: {
47 | width: "100%",
48 | },
49 | paper: {
50 | width: "100%",
51 | marginBottom: theme.spacing(2),
52 | },
53 | table: {
54 | minWidth: 750,
55 | },
56 | visuallyHidden: {
57 | border: 0,
58 | clip: "rect(0 0 0 0)",
59 | height: 1,
60 | margin: -1,
61 | overflow: "hidden",
62 | padding: 0,
63 | position: "absolute",
64 | top: 20,
65 | width: 1,
66 | },
67 | }));
68 |
69 | export default function WorkspaceTable(props) {
70 | const classes = useStyles();
71 | const {
72 | onSelected,
73 | headers,
74 | selected,
75 | compact,
76 | onClick,
77 | renderCellValue,
78 | rows,
79 | totalRows,
80 | page,
81 | onChangePage,
82 | rowsPerPage,
83 | onChangeRowsPerPage,
84 | descendingComparator,
85 | } = props;
86 | const [order, setOrder] = React.useState("asc");
87 | const [orderBy, setOrderBy] = React.useState("calories");
88 |
89 | const handleRequestSort = (event, property) => {
90 | const ascending = orderBy === property && order === "asc";
91 | setOrder(ascending ? "desc" : "asc");
92 | setOrderBy(property);
93 | };
94 |
95 | const handleSelectAllClick = (event) => {
96 | if (event.target.checked) {
97 | const newSelection = rows.map((row) => row.id);
98 | onSelected(newSelection);
99 | } else {
100 | onSelected([]);
101 | }
102 | };
103 |
104 | // const makeHandleSelect = (name) => (event) => {
105 | // const selectedIndex = selected.indexOf(name);
106 | // let newSelected = [];
107 |
108 | // if (selectedIndex === -1) {
109 | // newSelected = newSelected.concat(selected, name);
110 | // } else if (selectedIndex === 0) {
111 | // newSelected = newSelected.concat(selected.slice(1));
112 | // } else if (selectedIndex === selected.length - 1) {
113 | // newSelected = newSelected.concat(selected.slice(0, -1));
114 | // } else if (selectedIndex > 0) {
115 | // newSelected = newSelected.concat(
116 | // selected.slice(0, selectedIndex),
117 | // selected.slice(selectedIndex + 1)
118 | // );
119 | // }
120 |
121 | // onSelected(newSelected);
122 | // };
123 |
124 | const emptyRows = rowsPerPage - rows.length;
125 |
126 | const makeHandleCellClick = (row, column) => () => {
127 | if (column.clickable) {
128 | onClick(row);
129 | }
130 | };
131 |
132 | const renderCells = (row, rowIndex) => (
133 |
134 | {headers.map((column, columnIndex) => (
135 |
136 | {renderCellValue(row, rowIndex, column, columnIndex)}
137 |
138 | ))}
139 |
140 | );
141 |
142 | return (
143 |
144 |
145 |
146 |
150 |
160 |
161 | {stableSort(
162 | rows,
163 | getComparator(
164 | descendingComparator ||
165 | defaultDescendingComparator,
166 | order,
167 | orderBy
168 | )
169 | ).map((row, index) => {
170 | const isItemSelected =
171 | selected.indexOf(row.id) >= 0;
172 |
173 | return (
174 |
181 | {/*
182 |
188 | */}
189 |
190 | {renderCells(row, index)}
191 |
192 | );
193 | })}
194 | {emptyRows > 0 && (
195 |
200 |
201 |
202 | )}
203 |
204 |
205 |
206 | onChangePage(newPage)}
213 | onChangeRowsPerPage={(event) =>
214 | onChangeRowsPerPage(parseInt(event.target.value, 10))
215 | }
216 | />
217 |
218 |
219 | );
220 | }
221 |
--------------------------------------------------------------------------------
/src/workspace/account/AccountFormDrawer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { extractValues } from "../common/RecordForm";
5 |
6 | import FormDrawer from "../common/FormDrawer";
7 |
8 | // text, large_text, number, date_picker, date_range_picker, switch, phone_number, email_address
9 | // multiple_options (multiselect), single_option (drop down)
10 | // lookup - organization, user, account
11 |
12 | // Only top level children can have quickAdd. Groups cannot have required, unique, multipleValues, mininmumValues, maximumValues.
13 | // Groups can have readOnly, hidden, tooltip
14 | // The same person can work in multiple organizations. But such cases are rare. Therefore, the system should be kept
15 | // simple and not accomodate such cases. given, there are other work arounds.
16 |
17 | // The user name should be unique across your organization.
18 | const groups = [
19 | {
20 | label: "Basic",
21 | children: [
22 | {
23 | label: "User Name",
24 | identifier: "userName",
25 | type: "text",
26 | required: true,
27 | readOnly: false,
28 | quickAdd: true,
29 | unique: false,
30 | hidden: false,
31 | tooltip: "The user name of the account.",
32 | multipleValues: false,
33 | defaultValue: "",
34 | validations: "isAlphanumeric,minLength:3,maxLength:30",
35 | validationErrors: {
36 | isAlphanumeric:
37 | "The user name can contain only letters and digits.",
38 | minLength: "The user name must be 3-30 characters long.",
39 | maxLength: "The user name must be 3-30 characters long.",
40 | },
41 | },
42 | {
43 | label: "First Name",
44 | identifier: "firstName",
45 | type: "text",
46 | required: true,
47 | readOnly: false,
48 | quickAdd: true,
49 | unique: false,
50 | hidden: false,
51 | tooltip: "The first name of the account.",
52 | multipleValues: false,
53 | defaultValue: "",
54 | validations: "isAlphanumeric,minLength:3,maxLength:30",
55 | validationErrors: {
56 | isAlphanumeric:
57 | "The first name can contain only letters and digits.",
58 | minLength: "The first name must be 3-30 characters long.",
59 | maxLength: "The first name must be 3-30 characters long.",
60 | },
61 | },
62 | {
63 | label: "Last Name",
64 | identifier: "lastName",
65 | type: "text",
66 | required: true,
67 | readOnly: false,
68 | quickAdd: true,
69 | unique: false,
70 | hidden: false,
71 | tooltip: "The last name of the account.",
72 | multipleValues: false,
73 | defaultValue: "",
74 | validations: "isAlphanumeric,minLength:3,maxLength:30",
75 | validationErrors: {
76 | isAlphanumeric:
77 | "The last name can contain only letters and digits.",
78 | minLength: "The last name must be 3-30 characters long.",
79 | maxLength: "The last name must be 3-30 characters long.",
80 | },
81 | },
82 | {
83 | label: "Email Address",
84 | identifier: "emailAddress",
85 | type: "email_address",
86 | required: true,
87 | readOnly: false,
88 | quickAdd: true,
89 | unique: false,
90 | hidden: false,
91 | tooltip: "The email address of the account.",
92 | multipleValues: true,
93 | defaultValue: "",
94 | validations: "isEmail",
95 | validationErrors: {
96 | isEmail: "Please enter a valid email address.",
97 | },
98 | },
99 | {
100 | label: "Phone Number",
101 | identifier: "phoneNumber",
102 | type: "phone_number",
103 | required: false,
104 | readOnly: false,
105 | quickAdd: true,
106 | unique: false,
107 | hidden: false,
108 | tooltip: "The phone number of the account.",
109 | multipleValues: true,
110 | defaultValue: "",
111 | },
112 | {
113 | label: "Address Line 1",
114 | identifier: "addressLine1",
115 | type: "text",
116 | required: false,
117 | readOnly: false,
118 | quickAdd: false,
119 | unique: false,
120 | hidden: false,
121 | tooltip: "The first line of address.",
122 | multipleValues: false,
123 | defaultValue: "",
124 | },
125 | {
126 | label: "Address Line 2",
127 | identifier: "addressLine2",
128 | type: "text",
129 | required: false,
130 | readOnly: false,
131 | quickAdd: false,
132 | unique: false,
133 | hidden: false,
134 | tooltip: "The first line of address.",
135 | multipleValues: false,
136 | defaultValue: "",
137 | },
138 | {
139 | label: "City",
140 | identifier: "city",
141 | type: "text",
142 | required: false,
143 | readOnly: false,
144 | quickAdd: false,
145 | unique: false,
146 | hidden: false,
147 | tooltip: "The city.",
148 | multipleValues: false,
149 | defaultValue: "",
150 | },
151 | {
152 | label: "State",
153 | identifier: "state",
154 | type: "text",
155 | required: false,
156 | readOnly: false,
157 | quickAdd: false,
158 | unique: false,
159 | hidden: false,
160 | tooltip: "The state where the customer resides.",
161 | multipleValues: false,
162 | defaultValue: "",
163 | },
164 | {
165 | label: "Country",
166 | identifier: "country",
167 | type: "country",
168 | required: false,
169 | readOnly: false,
170 | quickAdd: false,
171 | unique: false,
172 | hidden: false,
173 | tooltip: "The country where the customer resides.",
174 | multipleValues: false,
175 | defaultValue: "",
176 | },
177 | {
178 | label: "Zip Code",
179 | identifier: "zipCode",
180 | type: "text",
181 | required: false,
182 | readOnly: false,
183 | quickAdd: false,
184 | unique: false,
185 | hidden: false,
186 | tooltip:
187 | "The zip code of the location where the customer resides.",
188 | multipleValues: false,
189 | defaultValue: "",
190 | },
191 | ],
192 | },
193 | ];
194 |
195 | function AccountFormDrawer(props) {
196 | const { title, onSave, showMore, open } = props;
197 |
198 | const values = props.account || extractValues(groups);
199 | return (
200 |
208 | );
209 | }
210 |
211 | AccountFormDrawer.propTypes = {
212 | title: PropTypes.string.isRequired,
213 | showMore: PropTypes.bool,
214 | account: PropTypes.object,
215 | onSave: PropTypes.func.isRequired,
216 | };
217 |
218 | AccountFormDrawer.defaultProps = {
219 | showMore: false,
220 | account: null,
221 | onCancel: null,
222 | };
223 |
224 | export default AccountFormDrawer;
225 |
--------------------------------------------------------------------------------
/src/workspace/common/WorkspaceFilter.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import FormControl from "@material-ui/core/FormControl";
3 | import InputLabel from "@material-ui/core/InputLabel";
4 | import Select from "@material-ui/core/Select";
5 | import MenuItem from "@material-ui/core/MenuItem";
6 | import Typography from "@material-ui/core/Typography";
7 | import Paper from "@material-ui/core/Paper";
8 | import Grid from "@material-ui/core/Grid";
9 | import Button from "@material-ui/core/Button";
10 | import { makeStyles } from "@material-ui/core/styles";
11 | import DateFnsUtils from "@date-io/date-fns";
12 | import {
13 | MuiPickersUtilsProvider,
14 | KeyboardDatePicker,
15 | } from "@material-ui/pickers";
16 |
17 | const useStyles = makeStyles((theme) => ({
18 | root: {
19 | padding: 16,
20 | marginLeft: 16,
21 | },
22 | title: {
23 | fontSize: 16,
24 | marginBottom: 24,
25 | },
26 | clear: {
27 | display: "block",
28 | marginLeft: "auto",
29 | marginTop: 16,
30 | },
31 | datePicker: {
32 | marginTop: theme.spacing(3),
33 | marginBottom: 0,
34 | },
35 | }));
36 |
37 | export function extractFilterState(fields) {
38 | const result = {};
39 | fields.forEach(
40 | (field) =>
41 | (result[field.identifier] = JSON.parse(
42 | JSON.stringify(field.defaultValue)
43 | ))
44 | );
45 | return result;
46 | }
47 |
48 | export function toURLParams(fields, values) {
49 | console.log(values);
50 |
51 | const result = {};
52 | fields.forEach((field) => {
53 | if (field.type === "time_range") {
54 | result[field.identifier] = values[field.identifier].option;
55 | if (values[field.identifier].option === "custom") {
56 | result[field.startIdentifier] = values[
57 | field.identifier
58 | ].startDate.getTime();
59 | result[field.endIdentifier] = values[
60 | field.identifier
61 | ].endDate.getTime();
62 | }
63 | } else {
64 | result[field.identifier] = values[field.identifier];
65 | }
66 | });
67 | console.log(result);
68 | return result;
69 | }
70 |
71 | export function toFilterState(fields, params) {
72 | const result = {};
73 | fields.forEach((field) => {
74 | if (field.type === "time_range") {
75 | result[field.identifier] = {};
76 |
77 | if (field.identifier in params) {
78 | result[field.identifier].option = params[field.identifier];
79 | } else {
80 | result[field.identifier].option = field.defaultValue.option;
81 | }
82 |
83 | if (field.startIdentifier in params) {
84 | const timestamp = parseInt(params[field.startIdentifier], 10);
85 | result[field.identifier].startDate = new Date(timestamp);
86 | } else {
87 | result[field.identifier].startDate =
88 | field.defaultValue.startDate;
89 | }
90 |
91 | if (field.endIdentifier in params) {
92 | const timestamp = parseInt(params[field.endIdentifier], 10);
93 | result[field.identifier].endDate = new Date(timestamp);
94 | } else {
95 | result[field.identifier].endDate = field.defaultValue.endDate;
96 | }
97 | } else {
98 | if (field.identifier in params) {
99 | result[field.identifier] = params[field.identifier];
100 | } else {
101 | result[field.identifier] = field.defaultValue;
102 | }
103 | }
104 | });
105 | return result;
106 | }
107 |
108 | export default function WorkspaceFilter(props) {
109 | const { fields, values, onValueChange, onClear } = props;
110 | const classes = useStyles();
111 | const makeChangeHandler = (field) => (event) => {
112 | onValueChange(field, event.target.value);
113 | };
114 | const makeTimeRangeHandler = (field) => (event) => {
115 | const newValue = Object.assign({}, values[field]);
116 | newValue.option = event.target.value;
117 | onValueChange(field, newValue);
118 | };
119 | const makeDateChangeHandler = (field, which) => (date) => {
120 | const newValue = Object.assign({}, values[field.identifier]);
121 | // ISO format
122 | newValue[which] = date; // format(date, "yyyy/MM/dd");
123 | onValueChange(field.identifier, newValue);
124 | };
125 |
126 | const renderSelect = (field, value) => (
127 |
132 | {field.title}
133 |
139 | {field.options.map((option) => (
140 | {option.title}
141 | ))}
142 |
143 |
144 | );
145 |
146 | const renderTimeRange = (field, value) => (
147 |
148 |
153 | {field.title}
154 |
160 | {field.options.map((option) => (
161 | {option.title}
162 | ))}
163 |
164 |
165 | {value.option === "custom" && (
166 |
167 |
182 |
197 |
198 | )}
199 |
200 | );
201 |
202 | return (
203 |
204 |
205 |
206 | Filters
207 |
208 |
209 | {fields.map((field) => (
210 |
211 | {field.type === "select" &&
212 | renderSelect(field, values[field.identifier])}
213 |
214 | {field.type === "time_range" &&
215 | renderTimeRange(
216 | field,
217 | values[field.identifier]
218 | )}
219 |
220 | ))}
221 |
222 |
229 | Clear
230 |
231 |
232 |
233 | );
234 | }
235 |
--------------------------------------------------------------------------------
/src/workspace/plan/PlanFormDrawer.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PropTypes from "prop-types";
3 |
4 | import { extractValues } from "../common/RecordForm";
5 | import FormDrawer from "../common/FormDrawer";
6 |
7 | const groups = [
8 | {
9 | label: "Basic",
10 | children: [
11 | {
12 | label: "Name",
13 | identifier: "name",
14 | type: "text",
15 | required: true,
16 | readOnly: false,
17 | quickAdd: true,
18 | unique: false,
19 | hidden: false,
20 | tooltip: "The name of the plan.",
21 | multipleValues: false,
22 | defaultValue: "",
23 | validations: "isAlphanumeric,minLength:2,maxLength:100",
24 | validationErrors: {
25 | isAlphanumeric:
26 | "The plan name can contain only letters and digits.",
27 | minLength: "The plan name must be 2-100 characters long.",
28 | maxLength: "The plan name must be 2-100 characters long.",
29 | },
30 | },
31 | {
32 | label: "Code",
33 | identifier: "code",
34 | type: "text",
35 | required: true,
36 | readOnly: false,
37 | quickAdd: true,
38 | unique: false,
39 | hidden: false,
40 | tooltip: "The code of the plan.",
41 | multipleValues: false,
42 | defaultValue: "",
43 | validations: "isAlphanumeric,minLength:2,maxLength:20",
44 | validationErrors: {
45 | isAlphanumeric:
46 | "The code name can contain only letters and digits.",
47 | minLength: "The code name must be 2-20 characters long.",
48 | maxLength: "The code name must be 2-20 characters long.",
49 | },
50 | },
51 | {
52 | label: "Description",
53 | identifier: "description",
54 | type: "large_text",
55 | required: false,
56 | readOnly: false,
57 | quickAdd: true,
58 | unique: false,
59 | hidden: false,
60 | tooltip: "The description of the plan.",
61 | multipleValues: false,
62 | defaultValue: "",
63 | rows: 4,
64 | validations: "maxLength:200",
65 | validationErrors: {
66 | maxLength: "The description must be 0-200 characters long.",
67 | },
68 | },
69 | {
70 | label: "Billing Cycle Period",
71 | identifier: "billingCyclePeriod",
72 | type: "number",
73 | required: true,
74 | readOnly: false,
75 | quickAdd: true,
76 | unique: false,
77 | hidden: false,
78 | tooltip: "The billing cycle period of the plan.",
79 | multipleValues: false,
80 | defaultValue: 0,
81 | validations: "isInt",
82 | validationErrors: {
83 | isInt: "Please enter a valid integer.",
84 | },
85 | },
86 | {
87 | label: "Billing Cycle Period Unit",
88 | identifier: "billingCyclePeriodUnit",
89 | type: "select",
90 | options: [
91 | {
92 | value: "days",
93 | title: "Days",
94 | },
95 | {
96 | value: "months",
97 | title: "Months",
98 | },
99 | ],
100 | required: true,
101 | readOnly: false,
102 | quickAdd: true,
103 | unique: false,
104 | hidden: false,
105 | tooltip: "The billing cycle period unit of the plan.",
106 | multipleValues: false,
107 | defaultValue: "days",
108 | },
109 | {
110 | label: "Price Per Billing Cycle",
111 | identifier: "pricePerBillingCycle",
112 | type: "number",
113 | required: true,
114 | readOnly: false,
115 | quickAdd: true,
116 | unique: false,
117 | hidden: false,
118 | tooltip:
119 | "The price per billing cycle for each unit of the plan.",
120 | multipleValues: false,
121 | defaultValue: 0,
122 | validations: "isNumeric",
123 | validationErrors: {
124 | isNumeric: "Please enter a valid number.",
125 | },
126 | },
127 | {
128 | label: "Setup Fee",
129 | identifier: "setupFee",
130 | type: "number",
131 | required: true,
132 | readOnly: false,
133 | quickAdd: true,
134 | unique: false,
135 | hidden: false,
136 | tooltip: "The setup fee of the plan.",
137 | multipleValues: false,
138 | defaultValue: 0,
139 | validations: "isNumeric",
140 | validationErrors: {
141 | isNumeric: "Please enter a valid number.",
142 | },
143 | },
144 | {
145 | label: "Total Billing Cycles",
146 | identifier: "totalBillingCycles",
147 | type: "number",
148 | required: true,
149 | readOnly: false,
150 | quickAdd: true,
151 | unique: false,
152 | hidden: false,
153 | tooltip: "The total number of billing cycles for the plan.",
154 | multipleValues: false,
155 | defaultValue: 0,
156 | validations: "isInt",
157 | validationErrors: {
158 | isInt: "Please enter a valid integer.",
159 | },
160 | },
161 | {
162 | label: "Trial Period",
163 | identifier: "trialPeriod",
164 | type: "number",
165 | required: true,
166 | readOnly: false,
167 | quickAdd: true,
168 | unique: false,
169 | hidden: false,
170 | tooltip: "The trial period of the plan.",
171 | multipleValues: false,
172 | defaultValue: 0,
173 | validations: "isInt",
174 | validationErrors: {
175 | isInt: "Please enter a valid integer.",
176 | },
177 | },
178 | {
179 | label: "Trial Period Unit",
180 | identifier: "trialPeriodUnit",
181 | type: "select",
182 | options: [
183 | {
184 | value: "days",
185 | title: "Days",
186 | },
187 | {
188 | value: "months",
189 | title: "Months",
190 | },
191 | ],
192 | required: true,
193 | readOnly: false,
194 | quickAdd: true,
195 | unique: false,
196 | hidden: false,
197 | tooltip: "The trial period unit of the plan.",
198 | multipleValues: false,
199 | defaultValue: "days",
200 | },
201 | {
202 | label: "Renew",
203 | identifier: "renew",
204 | type: "switch",
205 | required: true,
206 | readOnly: false,
207 | quickAdd: true,
208 | unique: false,
209 | hidden: false,
210 | tooltip:
211 | "Determines whether the subscription renews after the term.",
212 | multipleValues: false,
213 | defaultValue: false,
214 | },
215 | ],
216 | },
217 | ];
218 |
219 | function PlanFormDrawer(props) {
220 | const { title, onSave, showMore, open } = props;
221 |
222 | const values = props.plan || extractValues(groups);
223 | return (
224 |
232 | );
233 | }
234 |
235 | PlanFormDrawer.propTypes = {
236 | title: PropTypes.string.isRequired,
237 | showMore: PropTypes.bool,
238 | plan: PropTypes.object,
239 | onSave: PropTypes.func.isRequired,
240 | };
241 |
242 | PlanFormDrawer.defaultProps = {
243 | showMore: false,
244 | plan: null,
245 | onCancel: null,
246 | };
247 |
248 | export default PlanFormDrawer;
249 |
--------------------------------------------------------------------------------