Image for post
Image for post

A little explain… this guide is for building a Cafe or Restaurant Order Management App. This is the second part of the series . The Part One is for building the backend with django and rest framework and you can find it here. In this guide we will use React for building the frontend. If you dont want to create the api provided from the previous guide or you are here for React only, don’t worry will provide one API server!

One last note, before we continue, i want to inform you that i will keep the app as small as i can, so no authentication process, no Redux. I will try to make another guide which will contain anything on future. So lets start!

This guide will be splitted in steps.

  • Step 1. Create the project and the dependencies files
  • Step 2. Create the Homepage View
  • Step 3. Create the Order View
  • Step 4. Create the Products and Report View
  • Step 5. The Report Page
  • Step 6. Connect anything.

You can find all the code here → https://github.com/Zefarak/blog_pos_frontend

So just git clone it, npm install and its ready!

Step 1. Create the app.

First we hit create a new React app like on documentation

So

npx create-react-app blog_pos_frontend
cd blog_pos_frontend

Then we install the dependecies, so just replace the package.json file with this

{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"bootstrap": "^4.0.0-beta",
"history": "^4.7.2",
"jwt-decode": "^2.2.0",
"prop-types": "^15.6.2",
"react": "^15.6.1",
"react-dom": "^15.6.1",
"react-router-dom": "^4.3.1",
"react-scripts": "1.0.12",
"react-transition-group": "^1.1.2",
"reactstrap": "^4.8.0",
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}

And after that hit

npm install

On src/index.js replace the code with this

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import 'bootstrap/dist/css/bootstrap.css';
import App from './App';


ReactDOM.render((
<App />
), document.getElementById('root'));

And on App.js with this

import React from 'react';
import { BrowserRouter, Route, Switch} from 'react-router-dom';
import Homepage from './views/Homepage.js';
import Order from './views/Order.js';
import Report from './views/Reports.js';

class App extends React.Component{

render(){
return(
<BrowserRouter key='1'>
<Switch key='01'>

<Route key='002' exact path="/order/:id/" component={Order}/>
<Route key='003' exact path="/reports/" component={Report} />

<Route key='004' component={Homepage} />
</Switch>
</BrowserRouter>
)
}
}


export default App;

This is the routing. Here we say to React Router where to look when a specific url is used. When a url is described on path we used., then react selected to load the component we have bind on this url. For dynamic choosing url we add this /:id/ so fetch the correct data. Dont worry we are gonna create the views in a while!

And on public/index.html replace the code with this

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">

<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.1/css/all.css" integrity="sha384-5sAR7xN1Nv6T6+dT2mhtzEpVJvfS3NScPQTrOxhwjIuvcA67KV2R5Jz6kr4abQsz" crossorigin="anonymous">

<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>

<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
</body>
</html>

Then we will create three folders here on src the views, components and helpers. On Views will keep the React Components thats is stateful, on components will keep the stateless components mostly hehe and on helpers function or endpoints which we will need across the app.

So first go to helpers folder and start creating files. The first one will be called endpoint.js .In this file will store all endpoints urls and export them so if we want to change server or something whole application will be updated!

export const HOST = 'http://127.0.0.1:8000';

export const TABLES_ENDPOINT = HOST + '/api/table-list/';
export const ORDERS_ENDPOINT = HOST + '/api/order-list/';
export const PRODUCTS_ENDPOINT = HOST + '/api/product-list/';
export const ORDER_ITEMS_ENDPOINT = HOST + '/api/order-item-list';
export const ORDER_ITEM_ENDPOINT = HOST + '/api/order-item-detail/';
export const ORDER_ENDPOINT = HOST + '/api/order-detail/';
export const CATEGORYS_ENDPOINT = HOST + '/api/category-list/';
export const ORDER_REPORT_ENDPOINT = HOST + '/api/orders/reports/';
export const TABLE_DETAIL_ENDPOINT = HOST + '/api/order-item-detail';

I think all here is pretty obvious, on const HOST we will use the server url and then api endpoints we want to use. We do this to make a little dynamic all our urls when we want to change server. Now the second file will be called fetch_data.js. Lets see it.

if you dont want to create the api with django or you just dont care about backend just use this on first line

export const HOST = 'https://react-pos.herokuapp.com/api';

Lets continue..

import React from 'react';
import {Redirect} from 'react-router-dom';
import {ORDER_ITEM_ENDPOINT, ORDER_ITEMS_ENDPOINT} from "./endpoints";

export function lookupOptionsPOST(data) {
return {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}
}

export const lookupOptionsGET = {
method: "GET",
headers: {
'Content-Type': 'application/json'
}
};

export const lookupOptionsDEL = {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
};

export function lookupOptionsPUT(data) {
return {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}
}

export function putData(endpoint, data) {
let lookupOptions = {
method: 'PUT',
headers: {
'Content-Type':'application/json'
},
body: JSON.stringify(data)
};
fetch(endpoint, lookupOptions).then(
function(response) {
return response.json()
}
).then(
function(responseData) {
return <Redirect to='/' />;
}
)
}

export function fetchData(endpoint, thisComp, state, doneLoading) {
fetch(endpoint, lookupOptionsGET).then(
function (response) {
return response.json()
}
).then(
function (responseData) {
thisComp.setState({
[state]: responseData,
doneLoading: doneLoading
})

}
)
}

export function postQtyChange(action, id, thisComp) {
let item;
let data;
const endpoint = ORDER_ITEM_ENDPOINT + `${id}/`;
switch (action){
case 'ADD':
fetch(endpoint, lookupOptionsGET).then(
function(response) {
return response.json()
}
).then(
function(responseData) {
item = responseData;
data = {
id: item.id,
product_related: item.product_related,
order_related: item.order_related,
qty: item.qty + 1
};
fetch(endpoint, lookupOptionsPUT(data)).then(
function(response){
return response.json()
}
).then(
function(responseData){
thisComp.getOrderItems(item.order_related);
thisComp.getOrder(item.order_related)
}
)
}
);
break;
case 'REMOVE':
fetch(endpoint, lookupOptionsGET).then(
function(response) {
return response.json()
}
).then(
function(responseData) {
item = responseData;
data = {
id: item.id,
product_related: item.product_related,
order_related: item.order_related,
qty: item.qty - 1
};
fetch(endpoint, lookupOptionsPUT(data)).then(
function(response){
return response.json()
}
).then(
function(responseData){
thisComp.getOrderItems(item.order_related);
thisComp.getOrder(item.order_related)
}
)
}
);
break;
case 'DELETE':
fetch(endpoint, lookupOptionsDEL).then(
function(){
thisComp.componentDidMount()
}
);
break;
default:
thisComp.componentDidMount()
}
}


export function addOrEditProduct(order_id, product_id, thisComp) {
const endpoint = ORDER_ITEMS_ENDPOINT + `?product_related=${product_id}&order_related=${order_id}`;
fetch(endpoint, lookupOptionsGET).then(
function(response){
return response.json()
}
).then(function(responseData){
let data = {};
if (responseData.length > 0){
console.log('edit product', responseData);
data = {
id: responseData[0].id,
product_related: responseData[0].product_related,
order_related: responseData[0].order_related,
qty: responseData[0].qty + 1
};
console.log('edit product data', data);

fetch(ORDER_ITEM_ENDPOINT + `${responseData[0].id}/`, lookupOptionsPUT(data)).then(
function(response){
return response.json()
}
).then(function(responseData){thisComp.componentDidMount()})
} else {
console.log('new product');
const data = {
product_related: product_id,
order_related: order_id,
qty: 1
};

fetch(ORDER_ITEMS_ENDPOINT, lookupOptionsPOST(data)).then(
function(response){
return response.json()
}
).then(function(responseData){
thisComp.componentDidMount()
})
}
})
}

As you can see from the name of the file here we store all the connections to our API. So lets explain it.

function lookupOptionsPOST(data) {
return {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}
}

The first step we want is to create our lookupOptions for the fetching. The fetch library wants two argument the endpoint and the lookupOptions to work. The endpoints are created already and and we gonna create lookupOption for every main method for requesting data. The methods is ‘GET’, ‘POST’, ‘PUT’ and ‘DELETE’.

Now i will explain the steps of the lookupOptions. It’s a dictionary that first value is the method.

method: 'POST', or 'GET' or 'PUT' or 'DELETE'

Depends of what we want to do, we use the correct value.

  • Get data for API → ‘GET’
  • Create new data to API →’POST’
  • Modify data? → ‘PUT’
  • Delete data → ‘DELETE’

Then we fill the headers.

headers: {
'Content-Type': 'application/json'
},

This is always a must if your backend is a django server, its something django want to see, so put it always! Here depends on situation we can send the csrf-token or authentication token, but because doesnt needed here, no example!

And finally the body. Here we sent the data that we want to modify, update to create on server.We using the Json format to communicate.

body: JSON.stringify(data)

Lets see the GET fetch to understand what we did. So first we pass the url we want and use the lookupOptionsGET and send a request to that specific url on our API. If the API response positive we use the .then to collect the response data and to save then to state. The doneLoading is a variable for the state which informs the view, the fetching is complete and can render the results.

function fetchData(endpoint, thisComp, state, doneLoading) {
fetch(endpoint, lookupOptionsGET).then(
function (response) {
return response.json()
}
).then(
function (responseData) {
thisComp.setState({
[state]: responseData,
doneLoading: doneLoading
})

}
)
}

You just got a first taste about fetch etc the rest will explain it when we need to do a call.

And final the Navbar. Create this file in path src/compenets/Navbar.js.

import React from 'react';
import {
NavLink,
Collapse,
Navbar,
NavbarToggler,
NavbarBrand,
Nav,
NavItem} from 'reactstrap';


export default class MyNavbar extends React.Component {
constructor(props) {
super(props);
this.toggle = this.toggle.bind(this);
this.state = {
isOpen: false
};
}

toggle() {
this.setState({
isOpen: !this.state.isOpen
});
}

render() {
return (
<div>
<Navbar color="light" light>
<NavbarBrand href="/">reactstrap</NavbarBrand>
<NavbarToggler onClick={this.toggle} />
<Collapse isOpen={this.state.isOpen} navbar>
<Nav className="ml-auto" navbar>
<NavItem>
<NavLink href="/">Homepage</NavLink>
</NavItem>
<NavItem>
<NavLink href="/products/">Products</NavLink>
</NavItem>
<NavItem>
<NavLink href="/reports/">Reports</NavLink>
</NavItem>
</Nav>
</Collapse>
</Navbar>
</div>
);
}
}

Homepage view

Now lets create the views on views folder. The first View we gonna create is the Homepage.js . The page will render will be like this

Image for post
Image for post

Lets explain it line by line.

import React from 'react';
import PropTypes from 'prop-types';
import { withRouter, Redirect} from 'react-router-dom';
import { Container, Row, Col } from 'reactstrap'
import MyNavbar from '../components/Navbar.js';
import TableCart from '../components/TableCard.js'
import {fetchData} from '../helpers/fetch_data.js'
import {TABLES_ENDPOINT, ORDERS_ENDPOINT} from '../helpers/endpoints.js';
import {lookupOptionsPOST} from "../helpers/fetch_data";

First lets see the imports, React… no comment here!, then PropTypes is a library that validates our props when passing to a child component. Redirect and withRouter is coming from react Router and easy way to navigate between components and views. And because we will create our app using bootstrap will gonna use a library called reactstrap which combine bootstrap and react. The other imports is from our own code who have already created or we will create.

Lets see our first component now

class Homepage extends React.Component {

constructor(props) {
super(props);
this.state = {
tables: [],
doneLoading: false,
table: undefined,
new_order: false,
new_order_id:''
}
}

On state we will save our active tables. Then we check if the api fetch is ended from doneLoading and render the page with that tables. The last one we will store the data when we open a new table.

getTables() {
const thisComp = this;
fetchData(TABLES_ENDPOINT, thisComp, 'tables', true);
}

updateTables = () => {
this.getTables()
};

getTables()… No much there, we use the fetchdata and endpoint we created before, we pass this on our state with thisComp, and we set the doneLoading to true because is the last fetch we do on this component.

updateTables… a fast way to re-render the tables when we needed

newOrder = (id) => {
const thisComp = this;
const data = {
title: `Table ${id}`,
table: id,
active: true
};
fetch(ORDERS_ENDPOINT, lookupOptionsPOST(data)).then(
function(response) {
return response.json()
}
).then(function(responseData){
thisComp.setState({
new_order: true,
new_order_id: responseData.id
})
})
};

Then we use the newOrder, to create the data we want to send to API, after that we do a fetch and if anything goes well we change the state of new_order and new_order_id

componentDidMount(){
this.getTables();
setInterval(this.updateTables, 10000);
}

Here is to tell to component what we want to do when is loaded, with setInterval we force a update every 10 sec to see if the tables are free etc if we have multiple users here..

render() {
const doneLoading = this.state.doneLoading;
const tables = this.state.tables;
const {new_order} = this.state;
if(new_order) {
const new_url = `/order/${this.state.new_order_id}/`;
return <Redirect to={new_url} />
}
return (
<div>
<MyNavbar/>
<Container key={1}>{doneLoading !== false ? <MyContainer key={1} tables={tables} newOrder={this.newOrder} /> : <p>No data</p>}
</Container>
</div>
)
}

And final the render, First here we check if the new_order is true. If it is we force a redirect to new_order_id, keep in mind that the new_order is true only when a new order order created from table. And if not free we render the page. The page contains 2 major Components the Navbar and the Container. On Container we use the DoneLoading to check if the data is downloaded, so we don’t pass accidentally null or undefined data on our child components.

{doneLoading !== false ?

Ok now the other components

class MyContainer extends React.Component{

static propTypes = {
tables: PropTypes.array
};

render() {
const { tables } = this.props;

return (
<div>
<Row>
<Col xs="12">
<Row><h4 className='header'>Title</h4></Row>
<Row>
{ tables.map((table, index)=>(
<TableCart key={index} table={table} newOrder={this.props.newOrder} />
))
}
</Row>
</Col>
</Row>
</div>
)
}
}

No much explain for this Components is more presentation, we get the data and props from Homepage, we use the Row, Col… etc from reactstrap and the page is done!

Now lets see the TableCard. You can find it on folder or create it components/TableCard.js. Its a presentation component thats why is splitted.

import React from 'react';
import {Link} from 'react-router-dom';
import PropTypes from 'prop-types';
import { Col, Card, CardTitle, CardText, Button } from 'reactstrap'

export default class TableCard extends React.Component{

static propTypes = {
table: PropTypes.object
};

handleNewOrder = () => {
this.props.newOrder(this.props.table.id)
};


render() {
const {table} = this.props;
return (
<Col xs="6" sm="6">
<Card>
<CardTitle>{table.title}</CardTitle>
<CardText>Total value... {table.tag_value}</CardText>
{table.is_free ?
<Button onClick={this.handleNewOrder} color='success'>New Order </Button>
: <Link to={{
pathname: `/order/${table.active_order_id}/`
}}><Button color='info'>Details {table.active_order_id} </Button>
</Link>
}
</Card>
<br />
</Col>
)
}

}

Lets see what we did here

static propTypes = {
table: PropTypes.object
};

handleNewOrder = () => {
this.props.newOrder(this.props.table.id)
};

First we validate our data with PropTypes, PropTypes really help us when our app is on development mode, we can catch many errors! Then we created the handle new order. Is a fuction that inform the parent which id we used to create a new order and pass the data to the parent component

<Button onClick={this.handleNewOrder} color='success'>New Order </Button>

So when we click the Button we have attached the handleNewOrder , starts a domino which the id pass with the help of props from component to component, then the new_order function receive it, will fire the api fetch etc and when all fone the setState with the new data will update our view.

Step 2. The Order View

Image for post
Image for post

In this view will manage the order of the table we selected before. We will split the view in 2 parts. In first part we will manage the products and in the other the order. Lets create a new file on src/views/ called Order.js

You can find the code here.

import React from 'react';
import {withRouter} from 'react-router-dom';
import {Container, Row, Col} from 'reactstrap';
import MyNavbar from '../components/Navbar.js';
import ProductGrid from '../components/ProductTable.js'
import OrderDetails from '../components/OrderDetails.js'
import {fetchData, postQtyChange, putData, addOrEditProduct } from '../helpers/fetch_data.js'
import {ORDER_ITEMS_ENDPOINT, ORDER_ENDPOINT} from '../helpers/endpoints.js'


class Order extends React.Component{

constructor(props) {
super(props);
this.state = {
selected_categories: [],
order_data: '',
order_items:[],
order_id: '',
doneLoading: false
}
}

getOrderItems(id){
const endpoint = ORDER_ITEMS_ENDPOINT + '?order_related=' + id;
const thisComp = this;
fetchData(endpoint, thisComp, 'order_items', true)
}

getOrder(id){
const endpoint = ORDER_ENDPOINT + id + '/';
const thisComp = this;
fetchData(endpoint, thisComp, 'order_data', false);
}

changeQty = (action, item_id) => {
postQtyChange(action, item_id, this);
};

handleAddOrEditProduct = (product_id) => {
addOrEditProduct(this.state.order_data.id, product_id, this);
};

handleTableActions = (action) => {
const thisComp = this;
switch (action){
case 'CLOSE':
const order = this.state.order_data;
let data = {
id: order.id,
title: order.title,
table: order.table,
active: false,
};
const endpoint = ORDER_ENDPOINT + order.id + '/';
const lookupOptions = {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body:JSON.stringify(data)
}
fetch(endpoint, lookupOptions)
.then(resp => resp.json())
.then(repsData => {
thisComp.props.history.push('/')
})
break;
default:
thisComp.props.history.push('/')
}
};

componentDidMount(){
const {id} = this.props.match.params;
this.getOrder(id) ;
this.getOrderItems(id);
}

render() {
const doneLoading = this.state.doneLoading;
return(
<div>
<MyNavbar />
<Container>
{this.state.doneLoading ?
<Row>
<Col xs="6" sm="6">
<h4 className='header'>Products</h4>
{doneLoading ?
<ProductGrid
handleSelectedCategories={this.handleSelectedCategories}
handleAddOrEditProduct={this.handleAddOrEditProduct}
/>
: <p>No data</p>
}

</Col>
<Col xs="6" sm="6">
<h4 className='header'> Order Details </h4>
<br />
{doneLoading ?
<OrderDetails
order_data={this.state.order_data}
order_items={this.state.order_items}
handleTableActions={this.handleTableActions}
changeQty={this.changeQty}
/>
:
<p>No Data</p>
}
</Col>
</Row>
:
<Col xs={12}><p>No data</p></Col>
}
</Container>
</div>
)
}

}

export default withRouter(Order);

So lets the code.

constructor(props) {
super(props);
this.state = {
selected_categories: [],
order_data: '',
order_items:[],
order_id: '',
doneLoading: false
}
}

Not much here, selected_categories will be used for filtering the products, order_data, is order general data, order_items is the products already added. We save order_id on different field because we will get from props.

getOrderItems(id){
const endpoint = ORDER_ITEMS_ENDPOINT + '?order_related=' + id;
const thisComp = this;
fetchData(endpoint, thisComp, 'order_items', true)
}

getOrder(id){
const endpoint = ORDER_ENDPOINT + id + '/';
const thisComp = this;
fetchData(endpoint, thisComp, 'order_data', false);
}

The next two functions is API calls will be used on didComponentMount() like before. On getOrderItems we populate the items already added on our order and on getOrder we get all the data affect the Order.

Now lets see the function which we manipulate the data.

changeQty = (action, item_id) => {
postQtyChange(action, item_id, this);
};

handleAddOrEditProduct = (product_id) => {
addOrEditProduct(this.state.order_data.id, product_id, this);
};

handleTableActions = (action) => {
const thisComp = this;
switch (action){
case 'CLOSE':
const order = this.state.order_data;
let data = {
id: order.id,
title: order.title,
table: order.table,
active: false,
};
const endpoint = ORDER_ENDPOINT + order.id + '/';
putData(endpoint, data,);
break;
default:
console.log('oups!')
}
thisComp.props.history.push('/')
};

First is the changeQty, because i like Redux architect i will use the action part, if you dont what is redux no worries will explain. So depends what i want to do, i feed the action with the strings ‘ADD’ or ‘REMOVE’, and the id, then i call the function postQtyChange which you can find the source code in file helpers/fetch_data.js, here on line 74.

function postQtyChange(action, id, thisComp) {
let item;
let data;
const endpoint = ORDER_ITEM_ENDPOINT + `${id}/`;
switch (action){
case 'ADD':
fetch(endpoint, lookupOptionsGET).then(
function(response) {
return response.json()
}
).then(
function(responseData) {
item = responseData;
data = {
id: item.id,
product_related: item.product_related,
order_related: item.order_related,
qty: item.qty + 1
};
fetch(endpoint, lookupOptionsPUT(data)).then(
function(response){
return response.json()
}
).then(
function(responseData){
thisComp.getOrderItems(item.order_related);
thisComp.getOrder(item.order_related)
}
)
}
);
break;
case 'REMOVE':
fetch(endpoint, lookupOptionsGET).then(
function(response) {
return response.json()
}
).then(
function(responseData) {
item = responseData;
data = {
id: item.id,
product_related: item.product_related,
order_related: item.order_related,
qty: item.qty - 1
};
fetch(endpoint, lookupOptionsPUT(data)).then(
function(response){
return response.json()
}
).then(
function(responseData){
thisComp.getOrderItems(item.order_related);
thisComp.getOrder(item.order_related)
}
)
}
);
break;
case 'DELETE':
fetch(endpoint, lookupOptionsDEL).then(
function(){
thisComp.componentDidMount()
}
);
break;
default:
thisComp.componentDidMount()
}
}

So if action is ‘ADD’ we use the first case, which we fetch the data from server and then we add +1 in qty, we create the new data and we send them back to save them, and the server takes responsibility for anything else and update anything. If its remove , we just do the same but with -1 in qty!

Next line

handleAddOrEditProduct = (product_id) => {
addOrEditProduct(this.state.order_data.id, product_id, this);
};

Works similar way like before but here we do a little check here before, lets see it!

function addOrEditProduct(order_id, product_id, thisComp) {
const endpoint = ORDER_ITEMS_ENDPOINT + `?product_related=${product_id}&order_related=${order_id}`;
fetch(endpoint, lookupOptionsGET).then(
function(response){
return response.json()
}
).then(function(responseData){
let data = {};
if (responseData.length > 0){
console.log('edit product', responseData);
data = {
id: responseData[0].id,
product_related: responseData[0].product_related,
order_related: responseData[0].order_related,
qty: responseData[0].qty + 1
};
console.log('edit product data', data);

fetch(ORDER_ITEM_ENDPOINT + `${responseData[0].id}/`, lookupOptionsPUT(data)).then(
function(response){
return response.json()
}
).then(function(responseData){thisComp.componentDidMount()})
} else {
console.log('new product');
const data = {
product_related: product_id,
order_related: order_id,
qty: 1
};

fetch(ORDER_ITEMS_ENDPOINT, lookupOptionsPOST(data)).then(
function(response){
return response.json()
}
).then(function(responseData){
thisComp.componentDidMount()
})
}
})
}

Because this function we call it from products, we don’t want for example to end with 2 differents order item with same Product. For example. So we check if there is already a orderitem with the product we want to add,and if we have positive response we get that data like before and we add 1 in qty , if not we create the data from the begin and sent them to appropriate endpoint.

And the last one

handleTableActions = (action) => {
const thisComp = this;
switch (action){
case 'CLOSE':
const order = this.state.order_data;
let data = {
id: order.id,
title: order.title,
table: order.table,
active: false,
};
const endpoint = ORDER_ENDPOINT + order.id + '/';
putData(endpoint, data,);
break;
default:
console.log('oups!')
}
thisComp.props.history.push('/')
};

We use this for general table behavior, If the user press close sent data to endpoint saying active: false and then redirect to homepage and the table is open again!

Like i said before we split the view on two parts. First the products part.

import React from 'react';
import PropTypes from 'prop-types';
import { Table, Button } from 'reactstrap';
import Filters from './Filters.js'
import {fetchData} from "../helpers/fetch_data";
import {CATEGORYS_ENDPOINT, PRODUCTS_ENDPOINT} from "../helpers/endpoints";


export default class ProductGrid extends React.Component{

constructor(props){
super(props);
this.state = {
toggleForm: false,
products: [],
categories: [],
doneLoading: false
}
}

static propTypes = {
handleAddOrEditProduct: PropTypes.func.isRequired,
handleSelectedCategories: PropTypes.func.isRequired
}

getCategories(){
const endpoint = CATEGORYS_ENDPOINT;
const thisComp = this;
fetchData(endpoint, thisComp, 'categories')
}

getProducts(endpoint){
const thisComp = this;
fetchData(endpoint, thisComp, 'products')
}

handleSelectedCategories = (categories_list) =>{
if (categories_list){
const endpoint = PRODUCTS_ENDPOINT + '?category='+ categories_list;
console.log(endpoint);
this.getProducts(endpoint)
}
};

handleToggleForm = (e) => {
e.preventDefault();
this.setState({
toggleForm: !this.state.toggleForm
})
};

handleClearFilters = () => {
const endpoint = PRODUCTS_ENDPOINT;
this.setState({
toggleForm: false
});
this.getProducts(endpoint)
};

handleAddOrEditProduct = (id) => {
this.props.handleAddOrEditProduct(id)
}

componentDidMount(){
const endpoint = PRODUCTS_ENDPOINT;
this.getCategories();
this.getProducts(endpoint);
this.setState({
doneLoading: true
})
}

render(){
const {categories} = this.state;
if(this.state.toggleForm && categories.length > 0){
return(
<div>
<Button color='primary' onClick={this.handleToggleForm}>Close</Button>
<Filters
categories={categories}
handleSelectedCategories={this.handleSelectedCategories}
handleClearFilters={this.handleClearFilters}
clearFilters ={this.handleClearFilters}
/>
</div>
)
} else {
return(
<div>
<Button
color='primary'
onClick={this.handleToggleForm}
>Filters
</Button>

<ProductTable
products={this.state.products}
handleAddOrEditProduct={this.handleAddOrEditProduct}

/>
</div>
)
}
}
}

class ProductTable extends React.Component {

static propTypes = {
handleAddOrEditProduct: PropTypes.func.isRequired,
products: PropTypes.string.isRequired
}

handleAddOrEditProduct = (id) => {
this.props.handleAddOrEditProduct(id)
}

render(){
const products = this.props.products;
return(
<Table>
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Category</th>
<th>Value</th>
<th>#</th>
</tr>
</thead>
<tbody>
{products.map((product, index)=>(
<ProductTableTr
product={product}
handleAddOrEditProduct={this.handleAddOrEditProduct}
/>
))}
</tbody>
</Table>
)
}
}

class ProductTableTr extends React.Component{

static propTypes = {
handleAddOrEditProduct: PropTypes.func.isRequired,
product: PropTypes.object.isRequired
}

addProduct = () => {
this.props.handleAddOrEditProduct(this.props.product.id)
};

render(){
const {product} = this.props;
return(
<tr>
<td>{product.id}</td>
<td>{product.title}</td>
<td>{product.tag_category}</td>
<td>{product.tag_value}</td>
<td><Button color="success" onClick={this.addProduct}>Add</Button></td>
</tr>
)
}
}

Now lets see the ProductGrid, the link is here . First the imports, nothing new here.

import React from 'react';
import PropTypes from 'prop-types';
import { Table, Button } from 'reactstrap';
import Filters from './Filters.js'
import {fetchData} from "../helpers/fetch_data";
import {CATEGORYS_ENDPOINT, PRODUCTS_ENDPOINT} from "../helpers/endpoints";

Then we use the PropTypes to validate the data on props, and we create the api requests to populate the render page.

static propTypes = {
handleAddOrEditProduct: PropTypes.func.isRequired,
handleSelectedCategories: PropTypes.func.isRequired
}

getCategories(){
const endpoint = CATEGORYS_ENDPOINT;
const thisComp = this;
fetchData(endpoint, thisComp, 'categories')
}

getProducts(endpoint){
const thisComp = this;
fetchData(endpoint, thisComp, 'products')
}

Next the functions affects the filters.

handleSelectedCategories = (categories_list) =>{
if (categories_list){
const endpoint = PRODUCTS_ENDPOINT + '?category='+ categories_list;
console.log(endpoint);
this.getProducts(endpoint)
}
};

handleToggleForm = (e) => {
e.preventDefault();
this.setState({
toggleForm: !this.state.toggleForm
})
};

handleClearFilters = () => {
const endpoint = PRODUCTS_ENDPOINT;
this.setState({
toggleForm: false
});
this.getProducts(endpoint)
};

Let’s see it, the handleSelectedCategories we get the category we selected before and we do a new api call with filtering the data with it. Then the handleToggleForm informs the render if we want to see the filters or the products on the page. And last one we reset the categories with a clean call.

Next line is…

handleAddOrEditProduct = (id) => {
this.props.handleAddOrEditProduct(id)
}

Works similar way like the addOrEditProduct so let’s see the function. On helpers/fetch_data.js you will find this function.

function addOrEditProduct(order_id, product_id, thisComp) {
const endpoint = ORDER_ITEMS_ENDPOINT + `?product_related=${product_id}&order_related=${order_id}`;
fetch(endpoint, lookupOptionsGET).then(
function(response){
return response.json()
}
).then(function(responseData){
let data = {};
if (responseData.length > 0){
console.log('edit product', responseData);
data = {
id: responseData[0].id,
product_related: responseData[0].product_related,
order_related: responseData[0].order_related,
qty: responseData[0].qty + 1
};
console.log('edit product data', data);

fetch(ORDER_ITEM_ENDPOINT + `${responseData[0].id}/`, lookupOptionsPUT(data)).then(
function(response){
return response.json()
}
).then(function(responseData){thisComp.componentDidMount()})
} else {
console.log('new product');
const data = {
product_related: product_id,
order_related: order_id,
qty: 1
};

fetch(ORDER_ITEMS_ENDPOINT, lookupOptionsPOST(data)).then(
function(response){
return response.json()
}
).then(function(responseData){
thisComp.componentDidMount()
})
}
})
}

We pass the order id and the product id , to check if there is a OrderItem already created, and if there is we add 1 on qty if not we create one

Then is the componentDidMount which we call the api we need.

componentDidMount(){
const endpoint = PRODUCTS_ENDPOINT;
this.getCategories();
this.getProducts(endpoint);
this.setState({
doneLoading: true
})
}

And after that the render and the additional React Components

render(){
const {categories} = this.state;
if(this.state.toggleForm && categories.length > 0){
return(
<div>
<Button color='primary' onClick={this.handleToggleForm}>Close</Button>
<Filters
categories={categories}
handleSelectedCategories={this.handleSelectedCategories}
handleClearFilters={this.handleClearFilters}
clearFilters ={this.handleClearFilters}
/>
</div>
)
} else {
return(
<div>
<Button
color='primary'
onClick={this.handleToggleForm}
>Filters
</Button>

<ProductTable
products={this.state.products}
handleAddOrEditProduct={this.handleAddOrEditProduct}

/>
</div>
)
}
}
}

class ProductTable extends React.Component {

static propTypes = {
handleAddOrEditProduct: PropTypes.func.isRequired,
products: PropTypes.string.isRequired
}

handleAddOrEditProduct = (id) => {
this.props.handleAddOrEditProduct(id)
}

render(){
const products = this.props.products;
return(
<Table>
<thead>
<tr>
<th>Id</th>
<th>Title</th>
<th>Category</th>
<th>Value</th>
<th>#</th>
</tr>
</thead>
<tbody>
{products.map((product, index)=>(
<ProductTableTr
product={product}
handleAddOrEditProduct={this.handleAddOrEditProduct}
/>
))}
</tbody>
</Table>
)
}
}

class ProductTableTr extends React.Component{

static propTypes = {
handleAddOrEditProduct: PropTypes.func.isRequired,
product: PropTypes.object.isRequired
}

addProduct = () => {
this.props.handleAddOrEditProduct(this.props.product.id)
};

render(){
const {product} = this.props;
return(
<tr>
<td>{product.id}</td>
<td>{product.title}</td>
<td>{product.tag_category}</td>
<td>{product.tag_value}</td>
<td><Button color="success" onClick={this.addProduct}>Add</Button></td>
</tr>
)
}

On every level we pass the data or the functions we need, from props and when we are on the last nested component the ProductTableTr, we add the button that does the calling to add data to the Order.

if(this.state.toggleForm && categories.length > 0){

With the above line we control if we see the products or the filters, if toggleForm is true, then we caall the Filters. So on src/components and a new file Filter.js with this code.

import React from 'react';
import PropTypes from 'prop-types';
import { Button, Form, FormGroup } from 'reactstrap';


class Filters extends React.Component {

constructor(props) {
super(props);
this.state = {
categories: this.props.categories||[],
selected_categories: ''
}
}


static propTypes = {
categories: PropTypes.array.isRequired,
clearFilters: PropTypes.func.isRequired,
handleSelectedCategories: PropTypes.func.isRequired
};

handleClear = () => {
this.setState({
selected_categories: ''
});
this.props.clearFilters()

};

handleSelectedCategories = (id) => {
this.setState({
selected_categories: id
});
this.props.handleSelectedCategories(id);
console.log(this.state);
};

render() {
const categories = this.state.categories;
return (
<Form>
{categories.map((category, index)=>(
<CheckBoxComponent
field={category}
handleSelectedCategories={this.handleSelectedCategories}
checked={category.id === this.state.selected_categories ? true: false}
/>
))}
<Button onClick={this.handleClear} color='danger'>Clear Filters</Button>
</Form>
)
}
}

class CheckBoxComponent extends React.Component{

state = {
field: this.props.field||''
};

static propTypes = {
handleSelectedCategories: PropTypes.func,
field: PropTypes.object,
checked: PropTypes.bool
};

handleChange = (e) => {
this.props.handleSelectedCategories(this.state.field.id)
};

render(){
const {field} = this.state;
return(
<FormGroup check>
<label>{field.title}</label>
<input
type='radio'
className='form-control'
checked={this.props.checked}
onClick={this.handleChange}
/>
</FormGroup>
)
}

}

export default Filters

Works like before, first we check the props, and using the functions handleClear andhandleChange will send the data like before throught the props. Same happens on CheckBoxComponent.

The second part is controlled from OrderDetails Component.

import React from 'react';
import PropTypes from 'prop-types'
import { Table, Card, CardHeader,
CardTitle, CardText, Button } from 'reactstrap'

export default class OrderDetails extends React.Component{


static propTypes = {
handleTableActions: PropTypes.func,
handleBack: PropTypes.func,
order_items: PropTypes.array,
order_data: PropTypes.object,
changeQty: PropTypes.func.isRequired
};

handleCloseTable = () => {
this.props.handleTableActions('CLOSE')
};
handleBack = () => {
this.props.handleTableActions('BACK')
};

changeQty = (action, item_id) => {
this.props.changeQty(action, item_id)
}

render() {
const { order_items } = this.props;
const { order_data} = this.props;
return (
<div>
<Card>
<CardHeader>Table {order_data.tag_table}</CardHeader>
<CardTitle>Notes.. {order_data.title}</CardTitle>
<CardText>Value.. {order_data.tag_value}</CardText>
<button>{order_data.tag_status}</button>
<h4 className='header'>Order Items</h4>

<Table>
<thead>
<tr>
<th>Title</th>
<th>Qty</th>
<th>Value</th>
<th>Total Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{order_items.map((item, index)=>(
<OrderItem
item={item}
changeQty={this.changeQty}
/>
))}
</tbody>
</Table>
<Button color='warning' onClick={this.handleBack}>Back </Button>
</Card>
<Card>
<CardHeader>Actions</CardHeader>
<CardTitle>
<Button color='danger' onClick={this.handleCloseTable}>Close Table</Button>

</CardTitle>
</Card>

</div>
)
}
}

class OrderItem extends React.Component {

static propTypes = {
changeQty: PropTypes.func.isRequired,
item: PropTypes.object.isRequired
}

addQty = () => {
this.props.changeQty('ADD', this.props.item.id)
};

removeQty = () => {
this.props.changeQty('REMOVE', this.props.item.id)
};

handleDeleteItem = () => {
this.props.changeQty('DELETE', this.props.item.id)
};


render(){
const item = this.props.item;
return (
<tr>
<td>{item.tag_product_related}</td>
<td>{item.qty}</td>
<td>{item.tag_value}</td>
<td>{item.tag_total_value}</td>
<td>
<button onClick={this.addQty} className="btn btn-success">
<i className="fa fa-angle-up" />
</button>
<button onClick={this.removeQty} className="btn btn-warning">
<i className="fa fa-angle-down" />
</button>
<button onClick={this.handleDeleteItem} className="btn btn-danger">
<i className="fa fa-trash-alt" />
</button>
</td>
</tr>
)
}

}

After the imports and the check on Props, lets see the new parts..

handleCloseTable = () => {
this.props.handleTableActions('CLOSE')
};
handleBack = () => {
this.props.handleTableActions('BACK')
};

With this two function we control the state of order. With Close table, we set the active to false, and the backend does the actions needed to free the table again, with handle back we just return back to the homepage

changeQty = (action, item_id) => {
this.props.changeQty(action, item_id)

We just pass the props from parent component to grandchild

Now on order item we have this

addQty = () => {
this.props.changeQty('ADD', this.props.item.id)
};

removeQty = () => {
this.props.changeQty('REMOVE', this.props.item.id)
};

handleDeleteItem = () => {
this.props.changeQty('DELETE', this.props.item.id)
};

That connect the function i said before i used the Redux logic, every function connect to the proper button and fire the domino props data we want to see.

Step 5. The Report Page

Image for post
Image for post

The last page we are gonna create is the Report.js. Lets explore it line by line..

import React from 'react';
import MyNavbar from '../components/Navbar.js';
import {Container, Row, Col} from 'reactstrap';
import {fetchData, lookupOptionsGET } from '../helpers/fetch_data.js'
import {withRouter} from "react-router-dom";
import ReportGrid from '../components/ReportGrid.js'
import ReportTotalData from "../components/ReportTotalData";
import {ORDER_REPORT_ENDPOINT, TABLES_ENDPOINT, ORDERS_ENDPOINT} from "../helpers/endpoints";

class
Report extends React.Component {

constructor(props) {
super(props);
this.state = {
orders: [],
tables: [],
date_start: new Date(),
date_end: new Date(),
reports:{
total: 0,
count: 0,
avg: 0
},
doneLoading: false
}
}

Again nothing new here, we have created fields on state for collecting the orders, the table, and getting results on reports.

Next is the functions which collect data from api.

getOrders(endpoint, type_=false){
const thisComp = this;
fetchData(endpoint, thisComp, 'orders', type_)
}

getTables(){
const endpoint = TABLES_ENDPOINT;
const thisComp = this;
fetchData(endpoint, thisComp, 'tables', true)
}


getReports(endpoint){
const thisComp = this;
fetch(endpoint, lookupOptionsGET).then(
function(reps){
return reps.json()
}
).then(
function(responseData){
const reports = {
total: responseData.total,
count: responseData.count,
avg: responseData.avg
};
thisComp.setState({
reports: reports
})
}
)
}

Here keep in mind we do two different api calls on Orders. The first getOrders return the data as list and the second getReports return the data as total.We could use javascript to sum all this data, but on python server we have access to do the calculations on database level, so its faster i think.

handleClearFilters = () => {
this.componentDidMount()
};

updateReport = (selected_category) =>{
const endpoint = ORDER_REPORT_ENDPOINT+ '?table=' + selected_category;
this.getReports(endpoint)
};

handleSelectedCategories = (selectedCategories) =>{
if(selectedCategories){
const endpoint = ORDERS_ENDPOINT + '?table=' + selectedCategories;
this.getOrders(endpoint)
}
};

The next functions is similar, its for filtering and the update report, updates the url to fetch the correct data

Finally the render

render() {
return(
<div>
<MyNavbar />
<Container>
<Row>
<Col xs="8">
<ReportGrid orders={this.state.orders} />
</Col>
<Col xs="4">
<ReportTotalData
categories={this.state.tables}
handleSelectedCategories={this.handleSelectedCategories}
updateReport={this.updateReport}
reports={this.state.reports}
handleClearFilters={this.handleClearFilters}
/>
</Col>
</Row>
</Container>
</div>
)
}

I want explain much on ReportTotal Data is like the Filters we met before with the total stats we passed after the fetch. Same the ReportGrid is like the ProductGrid we created before, a table that contains the data.

Step 6. Connects anything!

Now we created all the views lets connected them. So let’s create our Navbar.js

Is that file that always follow us on every Component. So on src/components/ and a new file Navbar.js.

import React from 'react';
import {
NavLink,
Collapse,
Navbar,
NavbarToggler,
NavbarBrand,
Nav,
NavItem} from 'reactstrap';


export default class MyNavbar extends React.Component {
constructor(props) {
super(props);
this.toggle = this.toggle.bind(this);
this.state = {
isOpen: false
};
}

toggle() {
this.setState({
isOpen: !this.state.isOpen
});
}

render() {
return (
<div>
<Navbar color="light" light>
<NavbarBrand href="/">reactstrap</NavbarBrand>
<NavbarToggler onClick={this.toggle} />
<Collapse isOpen={this.state.isOpen} navbar>
<Nav className="ml-auto" navbar>
<NavItem>
<NavLink href="/">Homepage</NavLink>
</NavItem>
<NavItem>
<NavLink href="/products/">Products</NavLink>
</NavItem>
<NavItem>
<NavLink href="/reports/">Reports</NavLink>
</NavItem>
</Nav>
</Collapse>
</Navbar>
</div>
);
}
}

Here all the job does the reactstrap, we just create navlinks like ordinary bootstrap ans we add the urls we want

Thats its,Thank you for reading, Just hit npm start on console and its ready!

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store