├── .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 | 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 | Error 404 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 | Error 500 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 | 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 | 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 | 99 |
100 | 110 | 111 | 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 | 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 | 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 | 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 | {/**/} 180 | 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 | 143 | 144 | ); 145 | 146 | const renderTimeRange = (field, value) => ( 147 |
148 | 153 | {field.title} 154 | 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 | 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 | --------------------------------------------------------------------------------