Salesperson Extension Using React

In this help document, we’ll explain the steps to install and configure React and build the settings widget required for the Salesperson extension using React. You’ll have to add the code for the UI components required for the extension in the settings widget.

You can follow the steps in this help document or refer to our git project on the Salesperson extension using React.

Prerequisite: You need to download and install Node.js and Node Package Manager (npm) in your device. You can install Node.js from the Node.js website.

Folders for React and Settings Widget

You have to create separate folders for React and the settings widget.

React

To create a folder for React:

  1. Install React globally in your device. Enter the following command in your terminal or command prompt.
 npx create-react-app salesperson-react-app

This command will automatically create the folder. In this case, salesperson-react-app. You can enter a folder name of your choice.

Settings Widget

To create the settings widget:

1. Install the Zoho Extension Toolkit (ZET) globally by entering the following command in your terminal or command prompt.

npx install -g zoho-extension-toolkit

2. Enter the command:

 zet init

3. Select Zoho Books from the list of options.

After creating the React project and the settings widget, your project’s structure should look similar to the project structure shown in the image below.

React project structure

Configure React

Now that you’ve installed React, created a React project, and the settings widget, you have to configure React so that the SDK methods for the widget can function properly. Here’s how:

1. Go to your React project folder > public > index.html. Paste the following code in the body tag of the index.html file. You can find this code by navigating to your widget folder > app > widget’s html file.

<script src="https://js.zohostatic.com/zohofinance/v1/zf_sdk.js"></script>

Refer to the image below to know where to paste the code.

Configure React

2. Go to your React project folder > src > package.json. Add the line homepage: “.” to the file. The homepage field is a configuration that helps React applications work correctly when deployed to sub-directories or specific hosting environments.

Refer to the image below to know where to paste the code.

Configure React

Build UI

To build the UI required for the Salesperson extension, you have to install Bootstrap, react-router-dom, and react-select on your device. To do this, enter the following commands in your terminal or command prompt.

Bootstrap

 npm i bootstrap

react-router-dom

npm i react-router-dom

react-select

npm i react-select

Create the following folders and files in the src folder as shown in the image below.

src folder

In the subsequent sections, we’ll explain the need for each file or folder and what code you have to paste into each file.

index.js

In the index.js file, you invoke a call to ReactDOM.render() and initialize ZFAPPS. Invoking ReactDOM.render() inside the index.js file initializes the rendering of your React components and brings your application’s user interface to life by injecting it into the HTML document.

Insight: ZFAPPS is the name of the Zoho Extension Toolkit (ZET) framework used by the Zoho Finance apps.

Paste the following code in the index.js file:

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import 'bootstrap/dist/css/bootstrap.css';
window.onload =  function() {
  const root = ReactDOM.createRoot(document.getElementById('root'));
    window.ZFAPPS.extension.init().then((Zapp) => {
      window.Zapp = Zapp;
      window.ZFAPPS.invoke('RESIZE', { height: '550px', width: '600px' }).then(() => {
        root.render(<App />)// This will not loaded more than ones .
      });
    });
}
reportWebVitals();

App.js

In the App.js file, you initialize the connection name, org-variable placeholder name, and the initial setup for salesperson widget like the account through which the salesperson gets, the expense account details, etc.

Also, the App.js file is used to route the view of Home.jsx. The App.js file:

1. Handle the routing of the application.

2. Specify which content or component should be displayed based on the current route or user interaction.

When a certain route is matched, the App.js file is responsible for rendering the content of the Home.jsx file.

Paste the following code in the app.js file:

import React, { PureComponent } from 'react';
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
import Home from './views/Home';
import { Loader } from './components/loader';
class App extends PureComponent {
  state = {
    connection_link_name: 'salesperson_commission_books',
    isLoading: false,
    orgVariablePlaceholder: 'vl__u76nt_data_store',
    status: 'sent',
    type: 'Percentage',
    specification_type: 'SubTotal'
  };
  async componentDidMount() {
    this.setState(state => ({ ...state, isLoading: true }))
    let { organization } = await window.ZFAPPS.get("organization");
    let domainURL = organization.api_root_endpoint;
    this.setState(state => ({ ...state, organization, domainURL }))
    // GET Paid Through Account
    try {
      let getPaidThroughAccount = await window.ZFAPPS.request({
        url: `${domainURL}/autocomplete/paidthroughaccountslist`,
        method: 'GET',
        url_query: [
          {
            key: 'organization_id',
            value: organization.organization_id,
          },
        ],
        connection_link_name: this.state.connection_link_name,
      });
      let { data: { body } } = getPaidThroughAccount;
      let { results } = JSON.parse(body);
      this.setState(state => ({ ...state, paidThroughArray: results }));
    } catch (err) {
      console.error(err);
    }
    // GET Expense Account
    let getExpenseAccount = {
      url: `${domainURL}/autocomplete/expenseaccountslist`,
      method: 'GET',
      url_query: [
        {
          key: 'organization_id',
          value: organization.organization_id,
        },
      ],
      connection_link_name: this.state.connection_link_name,
    };
    try {
      let { data: { body } } = await window.ZFAPPS.request(getExpenseAccount);
      let { results } = JSON.parse(body);
      this.setState(state => ({ ...state, expenseArray: results }));
    } catch (err) {
      console.error(err);
    }
    try {
      // GET Global Field Values
      let getOrgVariable = {
        url: `${domainURL}/settings/orgvariables/${this.state.orgVariablePlaceholder}`,
        method: 'GET',
        url_query: [
          {
            key: 'organization_id',
            value: organization.organization_id,
          },
        ],
        connection_link_name: this.state.connection_link_name,
      }
      let { data: { body }, } = await window.ZFAPPS.request(getOrgVariable)
      let { orgvariable: { value } } = JSON.parse(body);
      if (value !== "") {
        value = JSON.parse(value);
        let { status, expense_account, paid_through_account, type, commission, specification_type } = value
        this.setState(state => ({
          ...state, status,
          expense_account,
          paid_through_account,
          type,
          commission,
          specification_type,
        }))
      }
    }
    catch (e) {
      console.error(e);
    }
    this.setState({ isLoading: false })
  }
  render() {
    return (
      <>
        {this.state.isLoading ? <div class="w-100" Style="margin-top: 150px;"><Loader /></div> :
          <Router>
            <div className="App">
              <Routes >
                <Route exact path="/" element={<Home defaultOptions={this.state} />} />
              </Routes>
            </div>
          </Router>}
      </>
    );
  }
}
export default App;

Components Folder

The components folder is the centralized location for all reusable and shareable UI elements. For the Salesperson extension, the following components are required:

Paste the following code in the dropdown.jsx file:

import Select from 'react-select';
const DropDown = (props) => {
    let customStyles = {
        control: (provided,state) => {
            return {
                    ...provided,
                    width:"100%",
                    border: "1px solid #DEE1EE ",
                    'box-shadow': "none",
                    "cursor": "pointer",
                    "&:hover":{
                        borderColor: state.isFocused ? " #408dfb" : "#408dfb"
                    },
            };
        },
        
        placeholder: (provided) => {
            return { ...provided, "margin-left": "5px", "color": "#666666", cursor: 'pointer' };
        },
        menuList: (provided) => {
            return { ...provided, 'max-height': '180px' }
          },
          dropdownIndicator: (provided, state) => {
            return {
              ...provided,
              color: state.isFocused ? '#616E86 !important' : '#616E86',
              cursor: 'pointer',
            }
          },
        
        
    };
    return (
        <div class="col-sm-5">
         <Select 
         styles={customStyles}
         value={props.value}
         options={props.options}
         onChange={props.handleOptionChange}
         getOptionLabel={(option)=>option[props.OptionLabelPath]}
         getOptionValue={(option)=>option[props.OptionValuePath]}
        />
    </div>
    );
  }
  
  export default DropDown;
  

input.jsx

Paste the following code in the input.jsx file:

const Input = (props) => {
    return (
      <div class="col-sm-5">
           <input type="number" class="form-control text" id={props.id} value={props.value} 
             placeholder={props.placeholder} onInput={props.handleInputChange} />
      </div>
    );
  }
  
  export default Input;

loader.jsx

Paste the following code in the loader.jsx file:

const Loader = () => (
    <div class="loading">
        <div class="load-circle1"></div>
        <div class="load-circle2"></div>
        <div class="load-circle3"></div>
        <div class="load-circle4"></div>
        <div class="load-circle5"></div>
    </div>
);
export { Loader};

radioButton.jsx

Paste the following code in the readioButton.jsx file:

const RadioButton = (props) => {
  return (
    <div>
      <input class="form-check-input" type="radio" name={props.name} id={props.id} value={props.value}
        onChange={props.handleOptionChange} checked={props.checked} />
      <label class="form-check-label" htmlFor={props.id}>{props.label}</label>
    </div>
  );
}
export default RadioButton;

home.jsx

The home.jsx is present inside the views folder. This file contains the overall UI for the Salesperson extension. The code to store data in the global fields after an user installs the extension is available in this file.

Paste the following code in the home.jsx file:

import React, { Component, Fragment } from 'react';
import RadioButton from '../components/radioButton';
import Input from '../components/input';
import DropDown from '../components/dropdown';
class Home extends Component {
  state = {
    status: 'sent',
    type: 'Percentage',
    specification_type: 'SubTotal'
  }
  constructor(props) {
    super(props);
    this.props_option = this.props?.defaultOptions;
  }
  async componentDidMount() {
    
    let { status, expense_account, paid_through_account, type, commission, specification_type } = this.props_option
    await this.setState(state => ({
      ...state, status,
      type,
      commission,
      expense_account,
      specification_type,
      paid_through_account
    }))
    this.eventListenerSetup = false;
  
    // ON PRE SAVE CHECK
    window.Zapp.instance.on("ON_SETTINGS_WIDGET_PRE_SAVE", async () => {
      this.eventListenerSetup = true;
      if (this.state.commission !== "" && this.state.commission !== undefined) {
        if (this.state.status === 'paid') {
          let isError = await checkAccount("paid")
          if (isError) {
            return {
              "prevent_save": true
            };
          }
          else {
            await updateOrgVariable();
          }
        }
        else {
          let isError = await checkAccount("sent")
          if (isError) {
            return {
              "prevent_save": true
            };
          }
          else {
            await updateOrgVariable();
          }
        }
      }
    })
    //
    let checkAccount = async (status) => {
      if (this.state.expense_account === undefined) {
        await this.showErrorNotification("Please select the Expense Account")
        return true
      }
      if (status !== "paid" && this.state.paid_through_account === undefined) {
        await this.showErrorNotification("Please select the Paid Through Account")
        return true
      }

    }
    // UPDATE Global Fields
    let updateOrgVariable = async () => {
      let data = { "value": { ...this.state } }
      window.ZFAPPS.request({
        url: `${this.props_option.domainURL}/settings/orgvariables/${this.props_option.orgVariablePlaceholder}`,
        method: 'PUT',
        url_query: [
          {
            key: 'organization_id',
            value: this.props_option.organization.organization_id,
          },
        ],
        body: {
          mode: 'formdata',
          formdata: [
            {
              key: 'JSONString',
              value: JSON.stringify(data),
            },
          ],
        },
        connection_link_name: this.props_option.connection_link_name,
      })
    };
  }
  async delay()
  {
    return new Promise(resolve=>setTimeout(resolve,500));
  }
    // Error Notification 
  async showErrorNotification(msg){
    await window.ZFAPPS.invoke("SHOW_NOTIFICATION", { type: "error", message: msg });
  }
  //PaidThroughSelectionChange
  paidThroughSelectChange = (data) => {
    this.setState(state => ({ ...state, paid_through_account: data }), () => {
    });
  }
  //ExpenseSelectionChange
  expenseSelectChange = (data) => {
    this.setState(state => ({ ...state, expense_account: data }), () => {
    });
  }
  render() {
    return (
      <div>
        <h4 className='heading'>When do you wish to create the expenses Sales Person Commissions ?</h4>
        <div className='flex-row'>
          <RadioButton id="sent-radio" value="sent" label="When an Invoice is Sent" name="invoiceStatusGroup" handleOptionChange={(event) => { this.setState(state => ({ ...state, status: event.target.value })); }} checked={this.state.status === 'sent'}></RadioButton>
          <RadioButton id="paid-radio" value="paid" label="When an Invoice is Paid" name="invoiceStatusGroup" handleOptionChange={(event) => { this.setState(state => ({ ...state, status: event.target.value })) }} checked={this.state.status === 'paid'}></RadioButton>
        </div>
        <Fragment>
          <div className='container '>
            <div className='container '>
              <h5 className='sideheading'>Commission Type</h5>
              <div className="flex-row">
                <RadioButton id="percentage-radio" value="Percentage" name="comissionTypeGroup" label="Percentage" handleOptionChange={(event) => { this.setState(state => ({ ...state, type: event.target.value })) }} checked={this.state.type === 'Percentage'}></RadioButton>
                <RadioButton id="amount-radio" value="Amount" name="comissionTypeGroup" label="Amount" handleOptionChange={(event) => { this.setState(state => ({ ...state, type: event.target.value }),) }} checked={this.state.type === 'Amount'}></RadioButton>
              </div>
            </div>
            <div className='container'>
              <h5 className='sideheading'>Commission Rate </h5>
              <Input id="number-field" value={this.state.commission} placeholder={this.state.type} handleInputChange={(event) => { this.setState(state => ({ ...state, commission: event.target.value })) }}></Input>
            </div>
            {this.state.status === 'sent' &&
              <div className='container'>
                <h5 className='sideheading'>Select the paid through account for expense created</h5>
                <DropDown id="paidthroughAccount-dropdown" value={this.state.paid_through_account} options={this.props_option.paidThroughArray} OptionValuePath="id" OptionLabelPath="text" handleOptionChange={(option) => this.paidThroughSelectChange(option)}></DropDown>
              </div>
            }
            <div className='container'>
              <h5 className='sideheading'>Select the expense account</h5>
              <DropDown id="expenseAccount-dropdown" value={this.state.expense_account} options={this.props_option.expenseArray} OptionValuePath="id" OptionLabelPath="text" handleOptionChange={(option) => this.expenseSelectChange(option)}></DropDown>
            </div>
            <div className='container'>
              <h5 className='sideheading'>Commission Specification</h5>
              <div className="flex-row">
                <RadioButton id="subtotal-radio" value="SubTotal" name="specificationGroup" label="Commission on SubTotal" handleOptionChange={(event) => { this.setState(state => ({ ...state, specification_type: event.target.value })) }} checked={this.state.specification_type === 'SubTotal'}></RadioButton>
                <RadioButton id="total-radio" value="Total" name="specificationGroup" label="Commission on Total" handleOptionChange={(event) => { this.setState(state => ({ ...state, specification_type: event.target.value })) }} checked={this.state.specification_type === 'Total'}></RadioButton>
              </div>
            </div>
          </div>
        </Fragment>
      </div>
    );
  }
}
export default Home;

index.css

Paste the following code in the index.css file:

body {
  overflow: hidden !important;
  height: 100%;
  width: 100%;
  padding: 0;
  margin: 0;
  background: #FAFAFA;
  font-family: 'Inter';
  font-weight: 400;
  color: #444;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
::-webkit-scrollbar {
  width: 4px;
  height: 8px !important;
}
::-webkit-scrollbar-track {
  background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
  background: #d0d2d7;
}
::-webkit-scrollbar-thumb:hover {
  background: #888;
}
* {
  box-sizing: border-box;
}
#app {
  height: 100%;
}
/* Styles for input component */
.text
{
    display: block !important;
    width: 100% !important;
    padding: 5px 8px !important;
    font-size: 13px !important;
    line-height: 1.6 !important;
    color: #495057 !important;
    background-color: #fff !important;
    background-clip: padding-box !important;
    border: 1px solid #ced4da !important;
    border-radius: 6px !important;
    transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out !important;
    height: 34px !important;
}
.text:focus {
    color: #495057 !important;
    background-color: #fff !important;
    border-color: #408dfb !important;
    outline: 0 !important;
    box-shadow: 0 0 0 3px rgba(64,141,251,0.16) !important;
}
/* Styles for Input component ends*/
/* Styles for RadioButton component */
input[type=radio]:checked {
  background-color: #408dfb;
  border-color: #408dfb;
}
input[type=radio] {
  cursor: pointer;
  width: 14px;
  height: 14px;
  vertical-align: top;
  background-color: #fff;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  border: 1px solid #00000040;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
 
}
input[type=radio]:hover:enabled {
  border-color: #408dfb;
  outline: 0;
  box-shadow: 0 0 0 3px rgba(64,141,251,.16);
}
input[type=radio]:focus {
  outline: 0 !important;
  box-shadow: 0 0 0 3px rgba(64,141,251,0.16) !important;
}
.form-check-label {
  margin-left: 4px !important;
  margin-bottom: 0;
  cursor: pointer !important;
}
/* Styles for RadioButton component ends*/

/* Styles for Loading component */
.loading {
  text-align: center;
}
.load-circle1,
.load-circle2,
.load-circle3,
.load-circle4,
.load-circle5 {
  width: 8px;
  height: 8px;
  background: grey;
  display: inline-block;
  border-radius: 20px;
  animation: loader 1.5s infinite;
  margin-right: 3px;
}
@keyframes loader {
  from {
    opacity: 1;
    scale: 1;
  }
  to {
    opacity: 0.25;
    scale: 0.3;
  }
}
.load-circle2 {
  animation-delay: 0.25s;
}
.load-circle3 {
  animation-delay: 0.5s;
}
.load-circle4 {
  animation-delay: 0.75s;
}
.load-circle5 {
  animation-delay: 1s;
}
/* Styles for Loading component ends*/
/* Styles for views Route */
.sideheading{
  font-weight: 400;
  font-size: 13px;
}
.sideheading:after {
  content: '*';
  color: red;
}
.heading{
  font-size: 16px;
  font-weight: 600;
}
.flex-row{
  display: flex;
  flex-direction: row;
  gap: 30px !important;
}
.container{
  margin-bottom: 10px;
  margin-top: 10px;
  margin-left: 0px !important;
}
/* Styles for Home views ends*/

Build Project

Now that you’ve made the necessary configurations for the React project, you’ll have to build it. To build the project, run the npm run build command in your terminal or command prompt. This will create an optimized build for the project in the build folder.

Paste the Build Folder Into the Widget Folder

After building the React project, you have to copy the build folder and paste it into the widget folder. To automate this process, you can create a file (say updateWidget.js) in the React folder, where you can implement the logic for copying and pasting.

Update the plugin-manifest.json File

The widget creation process happens during the extension’s installation. By default, the widget’s location will be the widgets pane in the right sidebar of the invoice creation page. To change its location, set the location attribute to plugin.globalfield and change the url to /app/index.html.

Additionally, you have to include the widget’s scope in the usedConnection array. To find the widget’s scope, go to the connection created for the extension in the Zoho Books Developer Portal and copy the JSON code. You have to paste this code inside the usedConnection array.

Refer to the image below to know where to paste the code.

plugin manifest file

Validate Widget

The next step is to validate the widget to ensure if it adheres to the guidelines specified in the plugin-manifest.json file. To do this, enter the command zet validate in the terminal or command prompt.

Pack Widget

To upload the widget in Zoho Books Developer Portal, you’ll have to pack it. Not all the files of your project directory are required while packing. Enter the command zet pack in your terminal or command prompt to pack the essential files and folders. After the command is executed, a ZIP file will be created.

Upload Widget

To upload your widget into Zoho Books Developer Portal:

With this, you’ve built the settings widget for the extension using React.


Was this document helpful?
Yes
No
Thank you for your feedback!
Want a feature?
Suggest
Switch to smart accounting software. Switch to Zoho Books.   Start my free 14-day trial Explore Demo Account

Books

Online accounting software
for small businesses.