DEV Community

Cover image for CRM App with React
Ajay Jarhad for Canonic Inc.

Posted on

CRM App with React

CRM or the Customer Relationship Management system is a great way to administrate the relationship with the customer. It’s a great tool to know and a pure pleasure to build.

In this article, we will discuss how to create a CRM app using React.js, Material UI, we will be also using other integrations such as Asana to create a ticket whenever a ticket is create on our app update a Google Sheet whenever a deal is added to the user.

Let's get started

  • Setting up React

Let's begin by creating a boilerplate using the
create-react-app

npx create-react-app crm-app
Enter fullscreen mode Exit fullscreen mode
  • Install Material UI

We will be using Material UI as an UI library. It boosts up the speed of development as we won't have to much of CSS manually.

npm install @mui/material @emotion/react @emotion/styled @material-ui/icons @mui/icons-material
Enter fullscreen mode Exit fullscreen mode
  • Create a ContentTable component

We are using a Table component by Material UI to quickly set up a table with pagination support, we will be using Custom pagination actions you can read more about it here.

This will give us a separate component. That handles pagination, for the sake of simplicity we will be extracting that function into another component.

To create your first component, create a directory inside src directory and call it components. Proceed by creating another directory inside ‘components’ and we will name it ContentTable, we will create a file with the same name inside of it, ContentTable.js.

    const customers = [
    {
    id: 1,
    name: "Lillian Carter",
    email: "xcollier@goodwin.com",
    phone: "+1-267-551-8666",
    company: "Larkin Group",
    label: "Marketing",
    },
    {
    id: 2,
    name: "Otto Walker",
    email: "stokes.hubert@hotmail.com",
    phone: "+1-580-977-4361",
    company: "Bednar-Sawayn",
    label: "Newsletter",
    },
    {
    id: 3,
    name: "Kaylee Taylor",
    email: "diana45@hotmail.com",
    phone: "+1-202-918-2132",
    company: "Rolfson and Sons ",
    label: "Ads",
    },
    {
    id: 4,
    name: "Aiden Houston",
    email: "ctromp@kassulke.info",
    phone: "+1-215-480-3687",
    company: "Wisoky, Windler and Nienow",
    label: "Newsletter",
    },
    {
    id: 5,
    name: "Davis Houston",
    email: "voreilly@yahoo.com",
    phone: "+1-203-883-5460",
    company: "Schmidt, Streich and Schuster",
    label: "Ads",
    },
];
Enter fullscreen mode Exit fullscreen mode

src/components/ListCustomer/ListCustomer.js

And, will pretty much copy the code for Table from Material UI’s documentation, and we will loop through our static array to fill the data in the table.

import { React, useState } from "react";

import {
  Table,
  TableBody,
  TableContainer,
  TableFooter,
  TablePagination,
  TableRow,
  Paper,
  TableCell,
  TableHead,
} from "@mui/material";

const customers = [
  {
    id: 1,
    name: "Lillian Carter",
    email: "xcollier@goodwin.com",
    phone: "+1-267-551-8666",
    company: "Larkin Group",
    label: "Marketing",
  },
  {
    id: 2,
    name: "Otto Walker",
    email: "stokes.hubert@hotmail.com",
    phone: "+1-580-977-4361",
    company: "Bednar-Sawayn",
    label: "Newsletter",
  },
  {
    id: 3,
    name: "Kaylee Taylor",
    email: "diana45@hotmail.com",
    phone: "+1-202-918-2132",
    company: "Rolfson and Sons ",
    label: "Ads",
  },
  {
    id: 4,
    name: "Aiden Houston",
    email: "ctromp@kassulke.info",
    phone: "+1-215-480-3687",
    company: "Wisoky, Windler and Nienow",
    label: "Newsletter",
  },
  {
    id: 5,
    name: "Davis Houston",
    email: "voreilly@yahoo.com",
    phone: "+1-203-883-5460",
    company: "Schmidt, Streich and Schuster",
    label: "Ads",
  },
];

const ContentTable = () => {
  const [page, setPage] = useState(0);
  const [rowsPerPage, setRowsPerPage] = useState(5);
  const emptyRows =
    page > 0 ? Math.max(0, (1 + page) * rowsPerPage - customers.length) : 0;

  const handleChangePage = (event, newPage) => {
    setPage(newPage);
  };

  const handleChangeRowsPerPage = (event) => {
    setRowsPerPage(parseInt(event.target.value, 10));
    setPage(0);
  };

  return (
    <TableContainer component={Paper} sx={{ margin: "2rem", width: "95%" }}>
      <Table sx={{ minWidth: 500 }} aria-label="custom pagination table">
        <TableHead>
          <TableRow>
            <TableCell
              align="left"
              sx={{
                backgroundColor: "black",
                color: "white",
                borderRight: "1px solid white",
              }}
            >
              Name
            </TableCell>
            <TableCell
              align="left"
              sx={{
                backgroundColor: "black",
                color: "white",
                borderRight: "1px solid white",
              }}
            >
              Company
            </TableCell>
            <TableCell
              align="left"
              sx={{
                backgroundColor: "black",
                color: "white",
                borderRight: "1px solid white",
              }}
            >
              Email
            </TableCell>
            <TableCell
              align="left"
              sx={{
                backgroundColor: "black",
                color: "white",
                borderRight: "1px solid white",
              }}
            >
              Phone
            </TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {customers &&
            (rowsPerPage > 0
              ? customers.slice(
                  page * rowsPerPage,
                  page * rowsPerPage + rowsPerPage
                )
              : customers
            ).map((row, index) => (
              <TableRow key={index}>
                <TableCell
                  component="th"
                  scope="row"
                  sx={{ width: 160, borderRight: "1px solid black" }}
                >
                  {row.name}
                </TableCell>
                <TableCell
                  sx={{ width: 160, borderRight: "1px solid black" }}
                  align="left"
                >
                  {row.company}
                </TableCell>
                <TableCell
                  sx={{ width: 160, borderRight: "1px solid black" }}
                  align="left"
                >
                  {row.email}
                </TableCell>
                <TableCell sx={{ width: 160 }} align="left">
                  {row.phone}
                </TableCell>
              </TableRow>
            ))}

          {emptyRows > 0 && (
            <TableRow style={{ height: 53 * emptyRows }}>
              <TableCell colSpan={6} />
            </TableRow>
          )}
        </TableBody>
        <TableFooter>
          <TableRow>
            <TablePagination
              rowsPerPageOptions={[5, 10, 25, { label: "All", value: -1 }]}
              colSpan={3}
              count={customers}
              rowsPerPage={rowsPerPage}
              page={page}
              SelectProps={{
                inputProps: {
                  "aria-label": "rows per page",
                },
                native: true,
              }}
              onPageChange={handleChangePage}
              onRowsPerPageChange={handleChangeRowsPerPage}
              ActionsComponent={TablePaginationActions}
            />
          </TableRow>
        </TableFooter>
      </Table>
    </TableContainer>
  );
};

export default ContentTable;

Enter fullscreen mode Exit fullscreen mode

src/components/ContentTable/ContentTable.js

The above code will complain about TablePaginationActions being undefined, so let’s handle that.

Next, we will create a utility component, that will help us with Pagination, Material UI uses it on same component as a demo, but extracting it outside makes ContentTable bit more clean.

Create a directory in componenets and name it Pagination and create a index.js file inside of it.

import { React } from "react";
import PropTypes from "prop-types";
import { useTheme } from "@mui/material/styles";
import { Box, IconButton } from "@mui/material";
import FirstPageIcon from "@mui/icons-material/FirstPage";
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import LastPageIcon from "@mui/icons-material/LastPage";

export const TablePaginationActions = (props) => {
  const theme = useTheme();
  const { count, page, rowsPerPage, onPageChange } = props;

  const handleFirstPageButtonClick = (event) => {
    onPageChange(event, 0);
  };

  const handleBackButtonClick = (event) => {
    onPageChange(event, page - 1);
  };

  const handleNextButtonClick = (event) => {
    onPageChange(event, page + 1);
  };

  const handleLastPageButtonClick = (event) => {
    onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
  };

  return (
    <Box sx={{ flexShrink: 0, ml: 2.5 }}>
      <IconButton
        onClick={handleFirstPageButtonClick}
        disabled={page === 0}
        aria-label="first page"
      >
        {theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
      </IconButton>
      <IconButton
        onClick={handleBackButtonClick}
        disabled={page === 0}
        aria-label="previous page"
      >
        {theme.direction === "rtl" ? (
          <KeyboardArrowRight />
        ) : (
          <KeyboardArrowLeft />
        )}
      </IconButton>
      <IconButton
        onClick={handleNextButtonClick}
        disabled={page >= Math.ceil(count / rowsPerPage) - 1}
        aria-label="next page"
      >
        {theme.direction === "rtl" ? (
          <KeyboardArrowLeft />
        ) : (
          <KeyboardArrowRight />
        )}
      </IconButton>
      <IconButton
        onClick={handleLastPageButtonClick}
        disabled={page >= Math.ceil(count / rowsPerPage) - 1}
        aria-label="last page"
      >
        {theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
      </IconButton>
    </Box>
  );
};

TablePaginationActions.propTypes = {
  count: PropTypes.number.isRequired,
  onPageChange: PropTypes.func.isRequired,
  page: PropTypes.number.isRequired,
  rowsPerPage: PropTypes.number.isRequired,
};

Enter fullscreen mode Exit fullscreen mode

src/components/Pagination/index.js

We can now import this Pagination Component in the ContentTable component and the error will go away. The end result should look like this

Image description

  • Create a Modal component

Now our table is ready, let’s work Modal component, we want this modal to pop up anytime a user clicks on any of the row on the table. We will be using Material UI’s Modal component for this you can read more about it here

In a modal, we want 3 columns:

  • 1st column would display more information about the particular customer
  • 2nd column would accommodate the notes feature, where a user would be able to leave notes on the customer.
  • 3rd column would a feature to add a deal to user and create a task on Asana.

It would look something like this

Image description

Let’s get to it.

import { React } from "react";
import { styled } from "@mui/material/styles";
import { Typography, Fade, Modal, Grid, Paper, Backdrop } from "@mui/material";

const Item = styled(Paper)(({ theme }) => ({
  ...theme.typography.body2,
  padding: theme.spacing(1),
  textAlign: "center",
  color: theme.palette.text.secondary,
  boxShadow: "none",
}));

export default function Modals() {
  return (
    <div>
      <Modal
        aria-labelledby="transition-modal-title"
        aria-describedby="transition-modal-description"
        open={open}
        closeAfterTransition
        BackdropComponent={Backdrop}
        BackdropProps={{
          timeout: 500,
        }}
      >
        <Fade in={open}>
          <Grid
            container
            spacing={1}
            sx={{
              width: "95%",
              height: "95%",
              backgroundColor: "white",
              position: "absolute",
              top: "4%",
              left: "3%",
            }}
          >
            <Grid item xs={3} sx={{ padding: "5px" }}>
              <Typography variant="h6" gutterBottom component="div">
                Name
              </Typography>
              <Typography variant="h6" gutterBottom component="div">
                Company
              </Typography>
              <Typography variant="h6" gutterBottom component="div">
                Phone
              </Typography>
              <Typography variant="h6" gutterBottom component="div">
                Email
              </Typography>
              <Typography variant="h6" gutterBottom component="div">
                Label
              </Typography>
            </Grid>
            <Grid
              item
              xs={6}
              sx={{ backgroundColor: "lightgray", padding: "5px" }}
            >
              <Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Notes
                </Typography>
              </Item>
            </Grid>
            <Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
              <Item>
                <Typography variant="h6" gutterBottom component="div">
                  Deals
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the revenue opportunities associated with this record
                </Typography>
              </Item>
              <Item sx={{ marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Tickets
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the customer requests associated with this record
                </Typography>
              </Item>
            </Grid>
          </Grid>
        </Fade>
      </Modal>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

src/components/Modal/Modal.js

We are heavily utilizing the Material UI’s Grid layout to make this column structure you can get more read on it here

The end result would look like this

Image description

But you have no way to see this, since nothing is triggering this component to open up. Let get that done.

We need to do 4 things in ContentTable component now:

  • Import the Modal component.
  • Create a state to hold the status of whether Modal is open or not.
  • Create a onClick trigger on the table’s row.
  • Feed the customer’s data to Modal component.
import Modals from "../Modal";

const [isOpen, setIsOpen] = useState();
const [customerData, setCustomerData] = useState();

const handleClick = (data) => {
    setIsOpen(true);
    setCustomerData(data);
    };

<TableRow
key={index}
onClick={() => {
    handleClick(row); //We are passing customer's information though this 'row' parameter
}}
sx={{ cursor: "pointer" }}
>

<Modals data={customerData} open={isOpen} setIsOpen={setIsOpen} />
Enter fullscreen mode Exit fullscreen mode

Now our task of setting up the modal is done. This is how the ContentTable component should look like:

import { React, useState } from "react";

import {
  Table,
  TableBody,
  TableContainer,
  TableFooter,
  TablePagination,
  TableRow,
  Paper,
  TableCell,
  TableHead,
} from "@mui/material";

import { TablePaginationActions } from "../Pagination";
import Modals from "../Modal";

const customers = [
  {
    id: 1,
    name: "Lillian Carter",
    email: "xcollier@goodwin.com",
    phone: "+1-267-551-8666",
    company: "Larkin Group",
    label: "Marketing",
  },
  {
    id: 2,
    name: "Otto Walker",
    email: "stokes.hubert@hotmail.com",
    phone: "+1-580-977-4361",
    company: "Bednar-Sawayn",
    label: "Newsletter",
  },
  {
    id: 3,
    name: "Kaylee Taylor",
    email: "diana45@hotmail.com",
    phone: "+1-202-918-2132",
    company: "Rolfson and Sons ",
    label: "Ads",
  },
  {
    id: 4,
    name: "Aiden Houston",
    email: "ctromp@kassulke.info",
    phone: "+1-215-480-3687",
    company: "Wisoky, Windler and Nienow",
    label: "Newsletter",
  },
  {
    id: 5,
    name: "Davis Houston",
    email: "voreilly@yahoo.com",
    phone: "+1-203-883-5460",
    company: "Schmidt, Streich and Schuster",
    label: "Ads",
  },
];

const ContentTable = () => {
  const [page, setPage] = useState(0);
  const [rowsPerPage, setRowsPerPage] = useState(5);
  const [isOpen, setIsOpen] = useState();
  const [customerData, setCustomerData] = useState();

  const emptyRows =
    page > 0 ? Math.max(0, (1 + page) * rowsPerPage - customers.length) : 0;

  const handleChangePage = (event, newPage) => {
    setPage(newPage);
  };

  const handleChangeRowsPerPage = (event) => {
    setRowsPerPage(parseInt(event.target.value, 10));
    setPage(0);
  };
  const handleClick = (data) => {
    setIsOpen(true);
    setCustomerData(data);
  };

  return (
    <TableContainer component={Paper} sx={{ margin: "2rem", width: "95%" }}>
      <Table sx={{ minWidth: 500 }} aria-label="custom pagination table">
        <TableHead>
          <TableRow>
            <TableCell
              align="left"
              sx={{
                backgroundColor: "black",
                color: "white",
                borderRight: "1px solid white",
              }}
            >
              Name
            </TableCell>
            <TableCell
              align="left"
              sx={{
                backgroundColor: "black",
                color: "white",
                borderRight: "1px solid white",
              }}
            >
              Company
            </TableCell>
            <TableCell
              align="left"
              sx={{
                backgroundColor: "black",
                color: "white",
                borderRight: "1px solid white",
              }}
            >
              Email
            </TableCell>
            <TableCell
              align="left"
              sx={{
                backgroundColor: "black",
                color: "white",
                borderRight: "1px solid white",
              }}
            >
              Phone
            </TableCell>
          </TableRow>
        </TableHead>
        <TableBody>
          {customers &&
            (rowsPerPage > 0
              ? customers.slice(
                  page * rowsPerPage,
                  page * rowsPerPage + rowsPerPage
                )
              : customers
            ).map((row, index) => (
              <TableRow
                key={index}
                onClick={() => {
                  handleClick(row);
                }}
                sx={{ cursor: "pointer" }}
              >
                <TableCell
                  component="th"
                  scope="row"
                  sx={{ width: 160, borderRight: "1px solid black" }}
                >
                  {row.name}
                </TableCell>
                <TableCell
                  sx={{ width: 160, borderRight: "1px solid black" }}
                  align="left"
                >
                  {row.company}
                </TableCell>
                <TableCell
                  sx={{ width: 160, borderRight: "1px solid black" }}
                  align="left"
                >
                  {row.email}
                </TableCell>
                <TableCell sx={{ width: 160 }} align="left">
                  {row.phone}
                </TableCell>
              </TableRow>
            ))}

          {emptyRows > 0 && (
            <TableRow style={{ height: 53 * emptyRows }}>
              <TableCell colSpan={6} />
            </TableRow>
          )}
        </TableBody>
        <TableFooter>
          <TableRow>
            <TablePagination
              rowsPerPageOptions={[5, 10, 25, { label: "All", value: -1 }]}
              colSpan={3}
              count={customers}
              rowsPerPage={rowsPerPage}
              page={page}
              SelectProps={{
                inputProps: {
                  "aria-label": "rows per page",
                },
                native: true,
              }}
              onPageChange={handleChangePage}
              onRowsPerPageChange={handleChangeRowsPerPage}
              ActionsComponent={TablePaginationActions}
            />
          </TableRow>
        </TableFooter>
      </Table>
      <Modals data={customerData} open={isOpen} setIsOpen={setIsOpen} />
    </TableContainer>
  );
};

export default ContentTable;
Enter fullscreen mode Exit fullscreen mode

src/components/ContentTable/ContentTable.js

Now if you click on any row on the table, the Modal we created should pop-up.

Now we are receiving 3 props in Modal component,

  • data - that contains the data of that row’s customer
  • open - is a state containing a boolean value of whether modal is open or not
  • setIsOpen - which is used to manipulate the value of ‘open’

Let’s utilize this.

export default function Modals({ data, open, setIsOpen }) 

const handleClose = () => {
    setIsOpen(!open);
    };

<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
    timeout: 500,
}}
>

<Grid item xs={3} sx={{ padding: "5px" }}>
<Typography variant="h6" gutterBottom component="div">
    {data.name}
</Typography>
<Typography variant="h6" gutterBottom component="div">
    {data.company}
</Typography>
<Typography variant="h6" gutterBottom component="div">
    {data.phone}
</Typography>
<Typography variant="h6" gutterBottom component="div">
    {data.email}
</Typography>
<Typography variant="h6" gutterBottom component="div">
    {data.label}
</Typography>
</Grid>
Enter fullscreen mode Exit fullscreen mode

Now our Modal component should look like this:

import { React } from "react";
import { styled } from "@mui/material/styles";
import { Typography, Fade, Modal, Grid, Paper, Backdrop } from "@mui/material";

const Item = styled(Paper)(({ theme }) => ({
  ...theme.typography.body2,
  padding: theme.spacing(1),
  textAlign: "center",
  color: theme.palette.text.secondary,
  boxShadow: "none",
}));

export default function Modals({ data, open, setIsOpen }) {
  const handleClose = () => {
    setIsOpen(!open);
  };

  return (
    <div>
      <Modal
        aria-labelledby="transition-modal-title"
        aria-describedby="transition-modal-description"
        open={open}
        onClose={handleClose}
        closeAfterTransition
        BackdropComponent={Backdrop}
        BackdropProps={{
          timeout: 500,
        }}
      >
        <Fade in={open}>
          <Grid
            container
            spacing={1}
            sx={{
              width: "95%",
              height: "95%",
              backgroundColor: "white",
              position: "absolute",
              top: "4%",
              left: "3%",
            }}
          >
            <Grid item xs={3} sx={{ padding: "5px" }}>
              <Typography variant="h6" gutterBottom component="div">
                **{data.name}**
              </Typography>
              <Typography variant="h6" gutterBottom component="div">
                **{data.company}**
              </Typography>
              <Typography variant="h6" gutterBottom component="div">
                **{data.phone}**
              </Typography>
              <Typography variant="h6" gutterBottom component="div">
                **{data.email}**
              </Typography>
              <Typography variant="h6" gutterBottom component="div">
                **{data.label}**
              </Typography>
            </Grid>
            <Grid
              item
              xs={6}
              sx={{ backgroundColor: "lightgray", padding: "5px" }}
            >
              <Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Notes
                </Typography>
              </Item>
            </Grid>
            <Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
              <Item>
                <Typography variant="h6" gutterBottom component="div">
                  Deals
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the revenue opportunities associated with this record
                </Typography>
              </Item>
              <Item sx={{ marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Tickets
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the customer requests associated with this record
                </Typography>
              </Item>
            </Grid>
          </Grid>
        </Fade>
      </Modal>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

src/components/Modal/Modal.js

The modal should have the dynamic data based on which row you click, and should look something like this:

Image description

  • Create a DetailsCard component.

Our bare-bones modal is ready, but it doesn’t look good at all, let’s extract out user’s information to a separate component, so we can enhance the looks.

Proceed by creating DetailsCard directory inside components, and create a DetailsCards.js inside it. We will be using Material UI’s Card component, you can read more about it here

import React from "react";

import { Fab, Typography, CardContent, Card } from "@mui/material";

const DetailsCard = () => {
    return (
    <>
        <Card sx={{ minWidth: 250, marginBottom: "2rem", marginTop: "2rem" }}>
        <CardContent>
            <Typography
            variant="body"
            gutterBottom
            component="div"
            sx={{ textAlign: "center", marginBottom: "1rem" }}
            >
            Personal Details
            </Typography>
            <Typography variant="h5" gutterBottom component="div">
            Name
            </Typography>
            <Typography variant="h6" gutterBottom component="div">
            Last Name
            </Typography>
        </CardContent>
        </Card>

        <Card sx={{ minWidth: 250, marginBottom: "2rem" }}>
        <CardContent>
            <Typography
            variant="body"
            gutterBottom
            component="div"
            sx={{ textAlign: "center", marginBottom: "1rem" }}
            >
            Professional Details
            </Typography>
            <Typography variant="h6" gutterBottom component="div">
            Company
            </Typography>
            <Typography variant="h6" gutterBottom component="div">
            Email
            </Typography>
        </CardContent>
        </Card>

        <Card sx={{ minWidth: 250 }}>
        <CardContent>
            <Typography
            variant="body"
            gutterBottom
            component="div"
            sx={{ textAlign: "center", marginBottom: "1rem" }}
            >
            Labels
            </Typography>
            <Fab variant="extended" size="small">
            Labels
            </Fab>
        </CardContent>
        </Card>
    </>
    );
};
export default DetailsCard;
Enter fullscreen mode Exit fullscreen mode

src/components/DetailsCard/DetailsCard.js

All we have to do now is import and feed the customer data as props in Modal component

The Modal component would look like this after importing and feeding in the customer data

import { React } from "react";
import { styled } from "@mui/material/styles";
import { Typography, Fade, Modal, Grid, Paper, Backdrop } from "@mui/material";

import DetailsCard from "../DetailsCard";

const Item = styled(Paper)(({ theme }) => ({
  ...theme.typography.body2,
  padding: theme.spacing(1),
  textAlign: "center",
  color: theme.palette.text.secondary,
  boxShadow: "none",
}));

export default function Modals({ data, open, setIsOpen }) {
  const handleClose = () => {
    setIsOpen(!open);
  };

  return (
    <div>
      <Modal
        aria-labelledby="transition-modal-title"
        aria-describedby="transition-modal-description"
        open={open}
        onClose={handleClose}
        closeAfterTransition
        BackdropComponent={Backdrop}
        BackdropProps={{
          timeout: 500,
        }}
      >
        <Fade in={open}>
          <Grid
            container
            spacing={1}
            sx={{
              width: "95%",
              height: "95%",
              backgroundColor: "white",
              position: "absolute",
              top: "4%",
              left: "3%",
            }}
          >
            <Grid item xs={3} sx={{ padding: "5px" }}>
              <DetailsCard
                name={data.name}
                company={data.company}
                email={data.email}
                phone={data.phone}
                labels={data.labels}
              />
            </Grid>
            <Grid
              item
              xs={6}
              sx={{ backgroundColor: "lightgray", padding: "5px" }}
            >
              <Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Notes
                </Typography>
              </Item>
            </Grid>
            <Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
              <Item>
                <Typography variant="h6" gutterBottom component="div">
                  Deals
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the revenue opportunities associated with this record
                </Typography>
              </Item>
              <Item sx={{ marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Tickets
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the customer requests associated with this record
                </Typography>
              </Item>
            </Grid>
          </Grid>
        </Fade>
      </Modal>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

src/components/Modal/Modal.js

And, after utilizing the received props, the DetailsCard component should look like this

import React from "react";

import { Fab, Typography, CardContent, Card } from "@mui/material";

const DetailsCard = ({ name, company, email, phone, labels }) => {
    return (
    <>
        <Card sx={{ minWidth: 250, marginBottom: "2rem", marginTop: "2rem" }}>
        <CardContent>
            <Typography
            variant="body"
            gutterBottom
            component="div"
            sx={{ textAlign: "center", marginBottom: "1rem" }}
            >
            Personal Details
            </Typography>
            <Typography variant="h5" gutterBottom component="div">
            {name}
            </Typography>
            <Typography variant="h6" gutterBottom component="div">
            {phone}
            </Typography>
        </CardContent>
        </Card>

        <Card sx={{ minWidth: 250, marginBottom: "2rem" }}>
        <CardContent>
            <Typography
            variant="body"
            gutterBottom
            component="div"
            sx={{ textAlign: "center", marginBottom: "1rem" }}
            >
            Professional Details
            </Typography>
            <Typography variant="h6" gutterBottom component="div">
            {company}
            </Typography>
            <Typography variant="h6" gutterBottom component="div">
            {email}
            </Typography>
        </CardContent>
        </Card>

        <Card sx={{ minWidth: 250 }}>
        <CardContent>
            <Typography
            variant="body"
            gutterBottom
            component="div"
            sx={{ textAlign: "center", marginBottom: "1rem" }}
            >
            Labels
            </Typography>
            <Fab variant="extended" size="small">
            {labels}
            </Fab>
        </CardContent>
        </Card>
    </>
    );
};
export default DetailsCard;
Enter fullscreen mode Exit fullscreen mode

src/components/DetailsCard/DetailsCard.js

The end result will look like this

Image description

Much better now!

Before we can proceed with Notes, we need to get our backend ready. As we will be storing the notes in database for data persistence. Thankfully, creating a backend on Canonic is a breeze

Time to get your APIs ready!

Getting your APIs for CRM app comes at real ease. All you have to do is clone this production-ready project on Canonic and you're done. It will provide you with the backend, APIs, and documentation you need for integration, without writing any code. You will need to create entries for customers on CMS, you can simply copy the contents from customer statics array we created.

Backend integration with GraphQL

Let's now integrate! Now that we have our APIs ready, let's move on by installing GraphQL packages.

  • Install GraphQL packages

To pull our data from the backend, we will need two packages - Apollo Client and GraphQL

npm i @apollo/client graphql
Enter fullscreen mode Exit fullscreen mode
  • Configure GraphQL to communicate with backend

Configure the Apollo Client in the project directory, inside index.js configure your apollo client so it would communicate with the backend.

Note to replace the uri with the one you'll get from Canonic.

import React from "react";
import ReactDOM from "react-dom";
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";

import ContentTable from "./components/ContentTable";

import "./index.css";

const client = new ApolloClient({
  uri: "https://crm-app.can.canonic.dev/graphql",
  cache: new InMemoryCache(),
});
ReactDOM.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <ContentTable />
    </ApolloProvider>
  </React.StrictMode>,
  document.getElementById("root")
);
Enter fullscreen mode Exit fullscreen mode

src/index.js

  • Query the data

For querying the data we will create a directory inside src called gql and create a file inside it called query.js. In it will write all the data we need from the backend.

import { gql } from "@apollo/client";

export const GET_CUSTOMERS = gql`
  query {
    customers {
      name
      _id
      createdAt
      updatedAt
      email
      phone
      company
      labels
      notes {
        description
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

src/gql/query.js

  • Propagate the API’s data.

Here we are propagating the data to ContentTable component. All we have to do is replace the customer static array we had with the code we get from API and replace all the references made to the static array with dynamic data.

import { React, useState } from "react";
import { useQuery } from "@apollo/client";
import {
  Table,
  TableBody,
  TableContainer,
  TableFooter,
  TablePagination,
  TableRow,
  Paper,
  TableCell,
  TableHead,
  CircularProgress,
} from "@mui/material";

import { GET_CUSTOMERS } from "../../gql/query";
import { TablePaginationActions } from "../Pagination";
import Modals from "../Modal";

export default function ContentTable() {
  const { data, loading } = useQuery(GET_CUSTOMERS);
  const [page, setPage] = useState(0);
  const [rowsPerPage, setRowsPerPage] = useState(5);
  const [customerData, setCustomerData] = useState();
  const [isOpen, setIsOpen] = useState();
  const emptyRows =
    page > 0
      ? Math.max(0, (1 + page) * rowsPerPage - data.customers.length)
      : 0;

  const handleChangePage = (event, newPage) => {
    setPage(newPage);
  };

  const handleChangeRowsPerPage = (event) => {
    setRowsPerPage(parseInt(event.target.value, 10));
    setPage(0);
  };

  const handleClick = (data) => {
    setIsOpen(true);
    setCustomerData(data);
  };

  return (
    <>
      {loading && (
        <CircularProgress
          sx={{ position: "absolute", top: "50%", left: "50%" }}
        />
      )}
      {!loading && (
        <TableContainer component={Paper} sx={{ margin: "2rem", width: "95%" }}>
          <Table sx={{ minWidth: 500 }} aria-label="custom pagination table">
            <TableHead>
              <TableRow>
                <TableCell
                  align="left"
                  sx={{
                    backgroundColor: "black",
                    color: "white",
                    borderRight: "1px solid white",
                  }}
                >
                  Name
                </TableCell>
                <TableCell
                  align="left"
                  sx={{
                    backgroundColor: "black",
                    color: "white",
                    borderRight: "1px solid white",
                  }}
                >
                  Company
                </TableCell>
                <TableCell
                  align="left"
                  sx={{
                    backgroundColor: "black",
                    color: "white",
                    borderRight: "1px solid white",
                  }}
                >
                  Email
                </TableCell>
                <TableCell
                  align="left"
                  sx={{
                    backgroundColor: "black",
                    color: "white",
                    borderRight: "1px solid white",
                  }}
                >
                  Phone
                </TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              {data &&
                (rowsPerPage > 0
                  ? data?.customers?.slice(
                      page * rowsPerPage,
                      page * rowsPerPage + rowsPerPage
                    )
                  : data.customers
                ).map((row, index) => (
                  <TableRow
                    key={index}
                    onClick={() => {
                      handleClick(row);
                    }}
                    sx={{ cursor: "pointer" }}
                  >
                    <TableCell
                      component="th"
                      scope="row"
                      sx={{ width: 160, borderRight: "1px solid black" }}
                    >
                      {row.name}
                    </TableCell>
                    <TableCell
                      sx={{ width: 160, borderRight: "1px solid black" }}
                      align="left"
                    >
                      {row.company}
                    </TableCell>
                    <TableCell
                      sx={{ width: 160, borderRight: "1px solid black" }}
                      align="left"
                    >
                      {row.email}
                    </TableCell>
                    <TableCell sx={{ width: 160 }} align="left">
                      {row.phone}
                    </TableCell>
                  </TableRow>
                ))}

              {emptyRows > 0 && (
                <TableRow style={{ height: 53 * emptyRows }}>
                  <TableCell colSpan={6} />
                </TableRow>
              )}
            </TableBody>
            <TableFooter>
              <TableRow>
                <TablePagination
                  rowsPerPageOptions={[5, 10, 25, { label: "All", value: -1 }]}
                  colSpan={3}
                  count={data?.customers?.length}
                  rowsPerPage={rowsPerPage}
                  page={page}
                  SelectProps={{
                    inputProps: {
                      "aria-label": "rows per page",
                    },
                    native: true,
                  }}
                  onPageChange={handleChangePage}
                  onRowsPerPageChange={handleChangeRowsPerPage}
                  ActionsComponent={TablePaginationActions}
                />
              </TableRow>
            </TableFooter>
          </Table>
          <Modals data={customerData} open={isOpen} setIsOpen={setIsOpen} />
        </TableContainer>
      )}
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

src/components/ContentTable/ContentTable.js

That’s all the changes we needed to integrate Canonic’s backend. Let get on to creating the Notes component.

  • Create Notes component

Before we create Notes component, let’s first prepare Modal component to display notes.

We will be doing these 3 things in Modal component

  • Creating a state to hold all our notes
  • Create a state to hold status on opening add a new note text box
  • Create a button that can switch between Displaying the notes to adding a note

We will faux import Notes component before it is created and feed in the data so when we finally create the component, we will already have a data to work with it

import Notes from "../Notes";
const [openAdd, setOpenAdd] = useState(false);
const [displayNotes, setDisplayNotes] = useState();
useEffect(() => {
    data && setDisplayNotes(data.notes);
}, [data]);

<Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
        {!openAdd && (
        <Box sx={{ flexGrow: 1 }}>
        <Paper
            elevation={1}
            sx={{
            padding: "2rem",
            height: "30rem",
            overflow: "auto",
            }}
        >
            {displayNotes?.map((note) => (
            <List>
                <ListItem sx={{ borderBottom: "1px solid black" }}>
                <ListItemText>{note.description}</ListItemText>
                </ListItem>
            </List>
            ))}
            {!openAdd && (
            <Button
                variant="outlined"
                startIcon={<AddIcon />}
                sx={{ marginTop: "1rem" }}
                onClick={() => setOpenAdd(true)}
                color="success"
            >
                Add a note
            </Button>
            )}
        </Paper>
        </Box>
    )}
    {openAdd && (
        <Notes
        setOpenAdd={setOpenAdd}
        _id={data._id}
        noteDescription={data?.notes?.map((item) => ({
            description: item.description,
        }))}
        setDisplayNotes={setDisplayNotes}
        />
    )}
Enter fullscreen mode Exit fullscreen mode

After this, the Modal component shall look like this

import React, { useState, useEffect } from "react";
import { styled } from "@mui/material/styles";
import {
  Typography,
  Fade,
  Modal,
  Grid,
  Paper,
  Backdrop,
  Box,
  Button,
  List,
  ListItem,
  ListItemText,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";

import DetailsCard from "../DetailsCard";
import Notes from "../Notes";

const Item = styled(Paper)(({ theme }) => ({
  ...theme.typography.body2,
  padding: theme.spacing(1),
  textAlign: "center",
  color: theme.palette.text.secondary,
  boxShadow: "none",
}));

export default function Modals({ data, open, setIsOpen }) {
  const [openAdd, setOpenAdd] = useState(false);
  const [displayNotes, setDisplayNotes] = useState();
  const handleClose = () => {
    setIsOpen(!open);
  };
  useEffect(() => {
    data && setDisplayNotes(data.notes);
  }, [data]);
  return (
    <div>
      <Modal
        aria-labelledby="transition-modal-title"
        aria-describedby="transition-modal-description"
        open={open}
        onClose={handleClose}
        closeAfterTransition
        BackdropComponent={Backdrop}
        BackdropProps={{
          timeout: 500,
        }}
      >
        <Fade in={open}>
          <Grid
            container
            spacing={1}
            sx={{
              width: "95%",
              height: "95%",
              backgroundColor: "white",
              position: "absolute",
              top: "4%",
              left: "3%",
            }}
          >
            <Grid item xs={3} sx={{ padding: "5px" }}>
              <DetailsCard
                name={data.name}
                company={data.company}
                email={data.email}
                phone={data.phone}
                labels={data.labels}
              />
            </Grid>
            <Grid
              item
              xs={6}
              sx={{ backgroundColor: "lightgray", padding: "5px" }}
            >
              <Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Notes
                </Typography>
                {!openAdd && (
                  <Box sx={{ flexGrow: 1 }}>
                    <Paper
                      elevation={1}
                      sx={{
                        padding: "2rem",
                        height: "30rem",
                        overflow: "auto",
                      }}
                    >
                      {displayNotes?.map((note) => (
                        <List>
                          <ListItem sx={{ borderBottom: "1px solid black" }}>
                            <ListItemText>{note.description}</ListItemText>
                          </ListItem>
                        </List>
                      ))}
                      {!openAdd && (
                        <Button
                          variant="outlined"
                          startIcon={<AddIcon />}
                          sx={{ marginTop: "1rem" }}
                          onClick={() => setOpenAdd(true)}
                          color="success"
                        >
                          Add a note
                        </Button>
                      )}
                    </Paper>
                  </Box>
                )}
                {openAdd && (
                  <Notes
                    setOpenAdd={setOpenAdd}
                    _id={data._id}
                    noteDescription={data?.notes?.map((item) => ({
                      description: item.description,
                    }))}
                    setDisplayNotes={setDisplayNotes}
                  />
                )}
              </Item>
            </Grid>
            <Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
              <Item>
                <Typography variant="h6" gutterBottom component="div">
                  Deals
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the revenue opportunities associated with this record
                </Typography>
              </Item>
              <Item sx={{ marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Tickets
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the customer requests associated with this record
                </Typography>
              </Item>
            </Grid>
          </Grid>
        </Fade>
      </Modal>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

src/components/Modal/Modal.js

We just now need to add the Mutation, that way we can send the new notes to our API

Let’s go back to GQL directory and create a file called Mutation.js

import { gql } from "@apollo/client";

export const ADD_NOTE = gql`
  mutation updateCustomerMutation($_id: ID!, $notes: [CustomerNoteInput!]!) {
    updateCustomer(_id: $_id, input: { notes: $notes }) {
      _id
      createdAt
      updatedAt
      name
      email
      phone
      company
      notes {
        description
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

src/gql/mutation.js

This mutations just updates the database with a new note whenever it is created.

Now we have Modal component conditioned, let’s begin creating Notes component. Create a directory inside components and name it Notes, create a file inside of it and call it Notes.js

import { useRef } from "react";
import { useMutation } from "@apollo/client";
import { Box, TextField, Grid, Paper, Stack, Button } from "@mui/material";
import LoadingButton from "@mui/lab/LoadingButton";
import SaveIcon from "@mui/icons-material/Save";
import { ADD_NOTE } from "../../gql/mutation";
const Notes = ({ _id, noteDescription, setOpenAdd, setDisplayNotes }) => {
  const descriptionRef = useRef();
  const [addNote] = useMutation(ADD_NOTE);

  const handleClick = () => {
    let description = descriptionRef.current.value;
    if (description.replace(/\s+/g, " ").length) {
      addNote({
        variables: {
          _id,
          notes: [...noteDescription, { description }],
        },
      });
      setDisplayNotes((oldList) => [...oldList, { description }]);
      setOpenAdd(false);
    }
  };
  const handleClose = () => {
    setOpenAdd(false);
  };

  return (
    <Box sx={{ flexGrow: 1 }}>
      <Paper elevation={6} sx={{ padding: "2rem" }}>
        <Stack spacing={2}>
          <Grid container sx={{ marginTop: "1rem" }}>
            <Grid item xs={12}>
              <TextField
                id="outlined-basic"
                label="Description"
                variant="outlined"
                multiline
                rows={2}
                sx={{ width: "100%" }}
                inputRef={descriptionRef}
              />
            </Grid>
          </Grid>
          <Grid sx={{ display: "flex", justifyContent: "space-between" }}>
            <LoadingButton
              loadingPosition="start"
              startIcon={<SaveIcon />}
              variant="outlined"
              sx={{ width: "25%" }}
              onClick={handleClick}
              color="success"
            >
              Save
            </LoadingButton>
            <Button
              variant="outlined"
              color="error"
              sx={{ width: "25%" }}
              onClick={handleClose}
            >
              Close
            </Button>
          </Grid>
        </Stack>
      </Paper>
    </Box>
  );
};

export default Notes;
Enter fullscreen mode Exit fullscreen mode

src/components/Notes/Notes.js

In here while updating the database with mutation, we are also updating displayNotes state, so the component would refresh and new note will be visible immediately

The end result should look like this

Image description

  • Create Deals componenet.

We are going to have a drop here where the user could set any deal to the customer, the data will get updated the database.

Let’s begin by defining a mutation for it. Navigate back to mutation.js that resides in gql directory.

import { gql } from "@apollo/client";

export const ADD_NOTE = gql`
  mutation updateCustomerMutation($_id: ID!, $notes: [CustomerNoteInput!]!) {
    updateCustomer(_id: $_id, input: { notes: $notes }) {
      _id
      createdAt
      updatedAt
      name
      email
      phone
      company
      notes {
        description
      }
    }
  }
`;

export const ADD_DEAL = gql`
  mutation updateDealsMutation($_id: ID!, $deals: [ID!]!) {
    updateDeal(_id: $_id, input: { deals: $deals }) {
      title
      amount
      dealOwner
      deals {
        _id
        name
        company
        phone
        email
      }
    }
  }
`;
Enter fullscreen mode Exit fullscreen mode

src/gql/mutation.js

We don’t need much from Modal in this case so let begin by creating Deals component, create a directory inside components name it Deals, create a file inside it and name it Deals.js

The flow is pretty much same as Notes in here, We are expecting a _id prop from Modal here.

Lets pass it

import React, { useState } from "react";
import { styled } from "@mui/material/styles";
import {
  Typography,
  Fade,
  Modal,
  Grid,
  Paper,
  Backdrop,
  Box,
  Button,
  List,
  ListItem,
  ListItemText,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";

import DetailsCard from "../DetailsCard";
import Notes from "../Notes";
import Deals from "../Deals";

const Item = styled(Paper)(({ theme }) => ({
  ...theme.typography.body2,
  padding: theme.spacing(1),
  textAlign: "center",
  color: theme.palette.text.secondary,
  boxShadow: "none",
}));

export default function Modals({ data, open, setIsOpen }) {
  const [openAdd, setOpenAdd] = useState(false);
  const [displayNotes, setDisplayNotes] = useState();
  const handleClose = () => {
    setIsOpen(!open);
  };

  return (
    <div>
      <Modal
        aria-labelledby="transition-modal-title"
        aria-describedby="transition-modal-description"
        open={open}
        onClose={handleClose}
        closeAfterTransition
        BackdropComponent={Backdrop}
        BackdropProps={{
          timeout: 500,
        }}
      >
        <Fade in={open}>
          <Grid
            container
            spacing={1}
            sx={{
              width: "95%",
              height: "95%",
              backgroundColor: "white",
              position: "absolute",
              top: "4%",
              left: "3%",
            }}
          >
            <Grid item xs={3} sx={{ padding: "5px" }}>
              <DetailsCard
                name={data.name}
                company={data.company}
                email={data.email}
                phone={data.phone}
                labels={data.labels}
              />
            </Grid>
            <Grid
              item
              xs={6}
              sx={{ backgroundColor: "lightgray", padding: "5px" }}
            >
              <Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Notes
                </Typography>
                {!openAdd && (
                  <Box sx={{ flexGrow: 1 }}>
                    <Paper
                      elevation={1}
                      sx={{
                        padding: "2rem",
                        height: "30rem",
                        overflow: "auto",
                      }}
                    >
                      {displayNotes?.map((note) => (
                        <List>
                          <ListItem sx={{ borderBottom: "1px solid black" }}>
                            <ListItemText>{note.description}</ListItemText>
                          </ListItem>
                        </List>
                      ))}
                      {!openAdd && (
                        <Button
                          variant="outlined"
                          startIcon={<AddIcon />}
                          sx={{ marginTop: "1rem" }}
                          onClick={() => setOpenAdd(true)}
                          color="success"
                        >
                          Add a note
                        </Button>
                      )}
                    </Paper>
                  </Box>
                )}
                {openAdd && (
                  <Notes
                    setOpenAdd={setOpenAdd}
                    _id={data._id}
                    noteDescription={data?.notes?.map((item) => ({
                      description: item.description,
                    }))}
                    setDisplayNotes={setDisplayNotes}
                  />
                )}
              </Item>
            </Grid>
            <Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
              <Item>
                <Typography variant="h6" gutterBottom component="div">
                  Deals
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the revenue opportunities associated with this record
                </Typography>
                <Deals userId={data._id} />
              </Item>
              <Item sx={{ marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Tickets
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the customer requests associated with this record
                </Typography>
              </Item>
            </Grid>
          </Grid>
        </Fade>
      </Modal>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

src/components/Modal/Modal.js

And, our Deals component is done. It should look like this

Image description

  • Create a Tickets component

This component will make a POST request to Asana API whenever a user inputs a title for the ticket, we are using create a task API for this you can read more about it here

It requires Title and either workspace or projects or parent defined in order to create a task

import { useRef, useState } from "react";
import { Box, TextField, Grid, Paper, Stack, Button } from "@mui/material";
import AddIcon from "@mui/icons-material/Add";
import DoneIcon from "@mui/icons-material/Done";

const Tickets = () => {
  const [addTicket, setAddTicket] = useState(false);
  const [addedTicketStatus, setAddedTicketStatus] = useState(false);
  const ticketNameRef = useRef();
  const handleAdd = () => {
    let data = {
      data: {
        name: ticketNameRef.current.value,
        workspace: process.env.REACT_APP_ASANA_WORKSPACE,
      },
    };
    if (!addedTicketStatus)
      fetch("https://app.asana.com/api/1.0/tasks", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
          Authorization: process.env.REACT_APP_ASANA_KEY,
        },
        body: JSON.stringify(data),
      })
        .then((res) => res.json())
        .then(() => setAddedTicketStatus(true));
  };
  const handleClose = () => {
    setAddTicket(false);
    setAddedTicketStatus(false);
  };
  return (
    <Box sx={{ flexGrow: 1 }}>
      {!addTicket && (
        <Button
          variant="outlined"
          color="success"
          onClick={() => setAddTicket(true)}
        >
          Add a ticket to Asana
        </Button>
      )}
      {addTicket && (
        <Paper elevation={6} sx={{ padding: "2rem" }}>
          <Stack spacing={2}>
            <Grid container sx={{ marginTop: "1rem" }}>
              <Grid item xs={12}>
                <TextField
                  id="outlined-basic"
                  label="Ticket name"
                  variant="outlined"
                  sx={{ width: "100%" }}
                  inputRef={ticketNameRef}
                />
              </Grid>
            </Grid>
            <Grid sx={{ display: "flex", justifyContent: "space-between" }}>
              <Button
                startIcon={addedTicketStatus ? <DoneIcon /> : <AddIcon />}
                variant="outlined"
                color="success"
                onClick={handleAdd}
                disabled={addedTicketStatus}
              >
                {addedTicketStatus ? "Added" : "Add"}
              </Button>
              <Button variant="outlined" color="error" onClick={handleClose}>
                Close
              </Button>
            </Grid>
          </Stack>
        </Paper>
      )}
    </Box>
  );
};

export default Tickets;
Enter fullscreen mode Exit fullscreen mode

src/components/Tickets/Tickets.js

We don’t need any props to be passed from Modal in Ticket component, so let’s just import it

import React, { useState } from "react";
import { styled } from "@mui/material/styles";
import {
  Typography,
  Fade,
  Modal,
  Grid,
  Paper,
  Backdrop,
  Box,
  Button,
  List,
  ListItem,
  ListItemText,
} from "@mui/material";
import AddIcon from "@mui/icons-material/Add";

import DetailsCard from "../DetailsCard";
import Notes from "../Notes";
import Deals from "../Deals";
import Tickets from "../Tickets";

const Item = styled(Paper)(({ theme }) => ({
  ...theme.typography.body2,
  padding: theme.spacing(1),
  textAlign: "center",
  color: theme.palette.text.secondary,
  boxShadow: "none",
}));

export default function Modals({ data, open, setIsOpen }) {
  const [openAdd, setOpenAdd] = useState(false);
  const [displayNotes, setDisplayNotes] = useState();
  const handleClose = () => {
    setIsOpen(!open);
  };

  return (
    <div>
      <Modal
        aria-labelledby="transition-modal-title"
        aria-describedby="transition-modal-description"
        open={open}
        onClose={handleClose}
        closeAfterTransition
        BackdropComponent={Backdrop}
        BackdropProps={{
          timeout: 500,
        }}
      >
        <Fade in={open}>
          <Grid
            container
            spacing={1}
            sx={{
              width: "95%",
              height: "95%",
              backgroundColor: "white",
              position: "absolute",
              top: "4%",
              left: "3%",
            }}
          >
            <Grid item xs={3} sx={{ padding: "5px" }}>
              <DetailsCard
                name={data.name}
                company={data.company}
                email={data.email}
                phone={data.phone}
                labels={data.labels}
              />
            </Grid>
            <Grid
              item
              xs={6}
              sx={{ backgroundColor: "lightgray", padding: "5px" }}
            >
              <Item sx={{ backgroundColor: "inherit", marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Notes
                </Typography>
                {!openAdd && (
                  <Box sx={{ flexGrow: 1 }}>
                    <Paper
                      elevation={1}
                      sx={{
                        padding: "2rem",
                        height: "30rem",
                        overflow: "auto",
                      }}
                    >
                      {displayNotes?.map((note) => (
                        <List>
                          <ListItem sx={{ borderBottom: "1px solid black" }}>
                            <ListItemText>{note.description}</ListItemText>
                          </ListItem>
                        </List>
                      ))}
                      {!openAdd && (
                        <Button
                          variant="outlined"
                          startIcon={<AddIcon />}
                          sx={{ marginTop: "1rem" }}
                          onClick={() => setOpenAdd(true)}
                          color="success"
                        >
                          Add a note
                        </Button>
                      )}
                    </Paper>
                  </Box>
                )}
                {openAdd && (
                  <Notes
                    setOpenAdd={setOpenAdd}
                    _id={data._id}
                    noteDescription={data?.notes?.map((item) => ({
                      description: item.description,
                    }))}
                    setDisplayNotes={setDisplayNotes}
                  />
                )}
              </Item>
            </Grid>
            <Grid item xs={3} sx={{ padding: "5px", marginTop: "2rem" }}>
              <Item>
                <Typography variant="h6" gutterBottom component="div">
                  Deals
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the revenue opportunities associated with this record
                </Typography>
                <Deals userId={data._id} />
              </Item>
              <Item sx={{ marginTop: "2rem" }}>
                <Typography variant="h6" gutterBottom component="div">
                  Tickets
                </Typography>
                <Typography variant="p" gutterBottom component="div">
                  Track the customer requests associated with this record
                </Typography>
                <Tickets />
              </Item>
            </Grid>
          </Grid>
        </Fade>
      </Modal>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

src/component/Modal/Modal.js

Now we have Tickets component ready, it should look like this

Image description

  • Create a Header component.

This will be a simple App bar that will display the a bar at the top of the page with the title of our project.

Let’s create a directory inside components and name it Header and a create file inside it and name Header.js

    import React from "react";

import { AppBar, Box, Toolbar, Typography } from "@mui/material";

const Header = () => {
  return (
    <Box sx={{ flexGrow: 1 }}>
      <AppBar position="static" sx={{ backgroundColor: "#000000" }}>
        <Toolbar>
          <Typography
            variant="h6"
            noWrap
            component="div"
            sx={{
              flexGrow: 1,
              display: { xs: "none", sm: "block" },
              textAlign: "center",
            }}
          >
            CRM
          </Typography>
        </Toolbar>
      </AppBar>
    </Box>
  );
};

export default Header;
Enter fullscreen mode Exit fullscreen mode

src/components/Header/Header.js

All we have to do now is to import it in index.js file

import React from "react";
import ReactDOM from "react-dom";
import { ApolloClient, InMemoryCache, ApolloProvider } from "@apollo/client";

import ContentTable from "./components/ContentTable";
import Header from "./components/Header";
import "./index.css";

const client = new ApolloClient({
  uri: "https://crm-app.can.canonic.dev/graphql",
  cache: new InMemoryCache(),
});
ReactDOM.render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <Header />
      <ContentTable />
    </ApolloProvider>
  </React.StrictMode>,
  document.getElementById("root")
);
Enter fullscreen mode Exit fullscreen mode

src/index.js

Voila! we are done.

sample code link.


BONUS

You can create log all the customer deals on Google Sheet.

Canonic has Google Sheet’s integrations, so all you have to do is open your cloned project, navigate to API, there select Deals table, Click on DB Triggers, there you will find a predefined Database trigger called UpdateTheSheet Click on Google Sheets webhook. You will have to authenticate yourself with a Google account. Then go to required and change the SpreadsheetID with your spreadsheet, make sure it is associated with the Google account you authenticated yourself with. You can change the range if you want. And, that’s it, you have successfully integrated Google Sheets into your CRM app now whenever user create a deal for a customer, DB trigger will update the Google Sheet.

Conclusion: It was a long journey well, I hope it was worth it. At the end you you learned how to created a feature packed awesome app, If you want, you can also duplicate this project from Canonic's sample app and easily get started by customizing it as per your experience. Check it out app.canonic.dev.

You can also check out our other guides here.

Join us on discord to discuss or share with our community. Write to us for any support requests at support@canonic.dev. Check out our website to know more about Canonic.

Latest comments (3)

Collapse
 
lalami profile image
Salah Eddine Lalami

check idurar (github.com/idurar/idurar-erp-crm) Open Source ERP/CRM Based on Mern Stack (Node.js / Express.js / MongoDb / React.js ) with Ant Design (AntD) and Redux

Image description

Collapse
 
rahulisversatile profile image
Rahul Sharma

HII Sir
I liked your ERP project very much, your project is worthy of praise and I thank you wholeheartedly for it. I am a beginning coder. Can you write a blog post on how to write this code step by step if it is helpful to you? By doing this it will be very convenient for the coder to build logic.
Your admirer.

Collapse
 
kuneism profile image
Kuneism

hi EDIE,
I loved your projects. basically i'm getting an error from this project could you please help me to find the solution.
{
"success": false,
"message": "Api url doesn't exist "
}