05Aug
Create simple POS with React.js, Node.js, and MongoDB #11: CRUD with Relation

Defenition: POS – “Point of Sale”. At the point of sale, the merchant calculates the amount owed by the customer, indicates that amount, may prepare an invoice for the customer (which may be a cash register printout), and indicates the options for the customer to make payment.

In the previous chapter, we successfully implemented CRUD operation for the supplier information of a grocery store. In this chapter, we are going to continue to implement CRUD operation for the Relation data and assign the POS machine to branch.

The key is to discover how to make relations on the database and deal with new UI change by adding multiple dropdowns. And lastly, display POS machine data in the branch index page.

Let’s get started!

First, we are going to start on the branch and POS machine.

Let us derive a relation from real-life for better understanding. We cannot place one machine to multiple branches but we can place multiple POS machines in the same branch as depicted in the diagram below:

Branch and POS relation diagram
Branch and POS relation diagram

Hence, we can define one too many relations.

Next, we move on to the coding implementations.

Updating Schema

Here, we are going to start from the backend by updating the database schema. We are going to add a relation between the POS machine schema in Branch schema. For that, we need to open the file branch.schema.js and add the code provided in the following code snippet:

const mongoose = require("mongoose");
const schema = mongoose.Schema({
    name: String,
    address: String,
    tel: String,
    frontimage: String,
    pos_machines: [
        { type: mongoose.Schema.Types.ObjectId, ref: 'pos_machines'    }
    ],
    created: { type: Date, default: Date.now },
});

module.exports = mongoose.model("branch", schema);

Next, we will start the update function from frontend to backend.

Create operation

In order to add the POS machine data to the branch, we are going to use the most popular dropdown component name React Select.

In frontend React project, we need to install this component first by running the following command in the project terminal:

yarn add react-select

In components/branch/create.js, we need to import the Select component from react-select package as shown in the code snippet below:

import Select from 'react-select'

Then, we will add it to somewhere on the form dropdown which will enable multiple selection following our first diagram. Next, we use the local state multiselect in order to handle the select options.

For the special part, we are going to create a new constant to handle options data separately from normal constant as shown in the code snippet below:

<Select
        value={multiselect}
        onChange={setMultiselect}
        isMulti
        closeMenuOnSelect={false}
        options={branchReducer.options
                ? branchReducer.options : null}
/>

Now, we add a new constant as shown in the code snippet below:

export const FETCHOPTION_SUCCESS = "FETCHOPTION_SUCCESS";

Then, we need to import the new constant to the branch reducer. Here, we are going to create a case like fetching data successfully but change index name to options in order to avoid the same data in the index page. For that, we need to use the code from the following code snippet:

import {
    BRANCH_FETCHING,
    BRANCH_SUCCESS,
    BRANCH_FAILED,
    BRANCH_CLEAR,
    FETCHOPTION_SUCCESS
} from "../constants";

...............

export default (state = initialState, { type, payload }) => {
    switch (type) {
      .................
        case FETCHOPTION_SUCCESS:
            return { ...state, isFetching: false, isError: false, options: payload };
        default:
            return state;
    }
};

Now in the branch.action.js file, we need to import new constant as shown in the code snippet below:

import {
    BRANCH_FETCHING,
    BRANCH_SUCCESS,
    BRANCH_FAILED,
    BRANCH_CLEAR, FETCHOPTION_SUCCESS
} from "../constants";

Then, we need to create a function to dispatch new event and pass options to state as shown in the code snippet below:

export const setFetchOptionStateToSuccess = (payload) => ({
    type: FETCHOPTION_SUCCESS,
    payload,
});

In order to use this, we need to create a new action for fetching POS machine data. After successful fetching, we need to clean data in order to match on react select as shown in the code snippet below:

export const getDropdownPOS = () => {
    return async (dispatch) => {
        dispatch(setBranchStateToFetching());
        const response = await httpClient.get(
            process.env.REACT_APP_API_URL + "branch_getpos"
        );
        if (response.data.result == "success") {
            let result = response.data.data.flat().map(item => {
                return {
                    value: item._id,
                    label: item.alias
                };
            })

            dispatch(setFetchOptionStateToSuccess(result));
        } else if (response.data.result === "error") {
            dispatch(setBranchStateToFailed());
            swal("Error!", response.data.message, "error");
        }
    };
};

Then, we will observe that we dispatched a new event and assign option data.

For backend, we need to create a new endpoint in api_branch.js. We must not forget to import the pos_machine schema file at the beginning.

By using the query in the code snippet below, we are going to select all of the data from the tables based on alias and column id:

router.get("/branch_getpos", async (req, res) => {
  try {
    let data = await posmachine.find({}).select({ "alias": 1, "_id": 1 }).sort({ created: -1 });
    res.json({
      result: "success",
      message: "Fetch Single Branch Successfully",
      data: data,
    });
  } catch (err) {
    console.log(err)
    res.json({ result: "error", message: err.msg });
  }
});

We have completed our backend configurations here. Now, we move to the front end to create the Component for it.

We need to dispatch a new action using useEffect in the create component as shown in the code snippet below:

useEffect(() => {
        dispatch(branchActions.getDropdownPOS())
}, []);

Hence, we have the dropdown component from the server as we can see in the screenshot below:

Dropdown from database
Dropdown from database

We are not done yet.

Next, we are going to save data with relation. We start by attaching new select data to formData. But before attaching, we need to clean data again by using the map function in order to keep id only as shown in the code snippet below:

      onSubmit={(values, { setSubmitting }) => {
                  let formData = new FormData();
                  formData.append("name", values.name);
                  formData.append("tel", values.tel);
                  formData.append("address", values.address);
                  let result = multiselect.map(arr => arr.value)

                  formData.append("pos_machines", result)
                  if(values.frontimage) {
                   formData.append("frontimage", values.frontimage);
                  }
        dispatch(branchActions.Create(formData, props.history));
                    setSubmitting(false);
     }}

On the backend, we start to update the create endpoint. Instead of throwing a request object to mongoose, we start to assign for an individual column.

Next, the POS machine-id that we get from the client is the plain text format. Hence, we need to split it and convert it into an array.

Then after creating a new branch, we query POS machine data and assign it to a branch object and update query again as directed in the code snippet below:

router.post("/branch", async (req, res) => {
  // console.log(req)
  try {
    var form = new formidable.IncomingForm();

    form.parse(req, async (err, fields, files) => {

      let newBranch = await branch.create({ name: fields.name, tel: fields.tel, address: fields.address });
      await uploadImage(files, newBranch);

      let pos_arr = fields.pos_machines.split(',')
      const pos = await posmachine.find().where('_id').in(pos_arr).exec();
      newBranch.pos_machines = pos
      await newBranch.save()
      res.json({ result: "success", message: "Create Brach data successfully" });
    });
  } catch (err) {
    res.json({ result: "error", message: err.msg });
  }
});

Hence, we are have successfully completed the Create operation.

Index Operation

After the Create operation is successful, we are going to redirect to the index page. Hence, we need to update the index page endpoint with a query for POS machine data.

Here, we use populate method to grab related data and send the response back to the client as shown in the code snippet below:

router.get("/branch", async (req, res) => {
  try {
    await branch.find({}).populate('pos_machines').exec(function (err, data) {
      if (err) {
        console.log(err);
      } else {
        res.json({
          result: "success",
          message: "Fetch Branch Successfully",
          data: data,
        });
      }
    });
  } catch (err) {
    res.json({ result: "error", message: err.msg });
  }
});

Now, we come back to the front end. We don’t need to do anything special in front end part now. We just need to create a new column in order to display POS alias and start iterating the data using map function as shown in the code snippet below:

<td>{data.name}</td>
 <td>{data.address}</td>
 <td>
    {data.pos_machines.map(value => {
      return value.alias + "," })
     }
</td>
 <td>{data.created}</td>

Hence, we can see create and index operations in action in the following simulation:

Show realation POS data on index
Show realation POS data on index

Our Index operation completes here. Now, we move on to the update operation.

Update Operation

For the update operation, we want to populate dropdown first. After that, we need to make the second request for populating another form in order to make the data selected.

The first network request is to populate the dropdown that we have created on Create operation.

The second request will mark the data as selected on the dropdown.

One interesting thing here is that we need to completely populate dropdown until we mark selected. We use then promise method to wait for the first request to complete before firing the second action as shown in the code snippet below:

export const getSingleBranch = (id) => {
    return async (dispatch) => {
        dispatch(setBranchStateToFetching());
        const response = await httpClient.get(
            process.env.REACT_APP_API_URL + "branch/" + id
        );

        if (response.data.result == "success") {
            dispatch(getDropdownPOS()).then(() => {
                dispatch(setBranchStateToSuccess(response.data.data));
            })

        } else if (response.data.result === "error") {
            dispatch(setBranchStateToFailed());
            swal("Error!", response.data.message, "error");
        }
    };
};

 

For backend, we need to update the mongoose query to populate relation document as shown in the code snippet below:

router.get("/branch/:id", async (req, res) => {
  try {
    await branch.findById({ _id: req.params.id })
      .populate('pos_machines').exec(function (err, data) {
        if (err) {
          console.log(err);
        } else {
          res.json({
            result: "success",
            message: "Fetch Single Branch Successfully",
            data: data,
          });
        }
      });
  } catch (err) {
    res.json({ result: "error", message: err.msg });
  }
});

The complicated task is not resolved yet. In UI, we need to wait until everything is done and marked selected.

In react-select, they provide us with the parameter named defaultValue to mark initial select but they did not provide any method to set defaultValue after page loading completes.

In order to resolve this, we need to hold on and wait until every server request has successfully loaded.

Then, we need to create separate render function as shown in the code snippet below:

const renderSelectwithSelected = () => {
        {
            if (branchReducer.result) {
                return (
                    <div class="form-group ">
                        <Select
                            name="pos_machines"
                            defaultValue={branchReducer.result
                                ? branchReducer.result.pos_machines.map(val => {
                                    return {
                                        'value': val._id,
                                        'label': val.alias
                                    }
                                }) : null}
                            // onChange={setMultiselect}
                            isMulti
                            closeMenuOnSelect={false}
                            options={branchReducer.options
                                ? branchReducer.options : null}
                        />
                    </div>

                )
            } else {
                return null; // or loading graphic
            }
        }
    }

Then, we need to attach it to the UI as shown in the code snippet below:

{renderSelectwithSelected()}
    <div class="form-group ">
        {showPreviewImage(values)}
    </div>

Hence, if we test it out, we will get the result as demonstrated in the following simulation:

Populate POS dropdown
Populate POS dropdown

Updating Data

In order to update the data on update operation, we need to perform some configurations in the on create operation.

First, we need to assign the selected option to formData by assigning it to the state.

Here, we are going to create a new state called multi-select using useState hook as shown in the code snippet below:

const [multiselect, setMultiselect] = useState([])

Then, we need to assign it to Select component as shown in the code snippet below:

 <Select
   name="pos_machines"
   onChange={setMultiselect}

And then, assign it to formData as shown in the code snippet below:

let result = multiselect.map(arr => arr.value)
formData.append("pos_machines", result)

On the backend, we just need to update the following create operation:

router.put("/branch", async (req, res) => {

  try {
    var form = new formidable.IncomingForm();

    form.parse(req, async (err, fields, files) => {
      let updateBranch = await branch.findByIdAndUpdate({ _id: fields.id }, { name: fields.name, tel: fields.tel, address: fields.address });
      let pos_arr = fields.pos_machines.split(',')
      const pos = await posmachine.find().where('_id').in(pos_arr).exec();
      updateBranch.pos_machines = pos
      await updateBranch.save()
      await uploadImage(files, updateBranch);
      res.json({ result: "success", message: "Update Brach data successfully" });
    });
  } catch (err) {
    res.json({ result: "error", message: err.msg });
  }
});

 

Hence, we will get the result as shown in the following simulation:

Update success
Update success

Delete Operation

The delete operation here is no different from the previous chapter. We do not need to make any updates to it.

Finally, we have successfully completed the CRUD operation with Relation data.

Conclusion

In this chapter, we learned a lot about MongoDB relations. We also learned how to update our UI to support relation data. We got guidance on how to use Select component from the react-select package as well.

In the next chapter, we will upgrade our table by integrating react-table package configurations.

Do not miss the codes for this chapter that are available on Github for both Frontend and Backend.

 

Developer Relation @instamobile.io

Rendering Patterns: Static and Dynamic Rendering in Nextjs

Next.js is popular for its seamless support of static site generation (SSG) and server-side rendering (SSR), which offers developers the flexibility to choose the most appropriate rendering method for each application page. Traditionally, Next.js utilized functions like getStaticProps, getStaticPaths, and getServerSideProps to manage how pages are rendered. Depending on the data requirements, these functions determine whether a page should be generated at build time or on each server request.

Leave a Reply