Building and generate Invoice PDF system with React.js, Redux, and Node.js can be a complex task, but I'm here to guide you through the process. Here's a step-by-step tutorial on how you can create such a system:
Github Repository : https://github.com/idurar/idurar-erp-crm
Step 1: Set up the environment
- Make sure you have Node.js installed on your machine.
- Create a new directory for your project and navigate into it using the terminal.
- Initialize a new Node.js project by running
npm init
. - Install required dependencies by running
npm install react redux react-redux
.
Step 2: Setting up the server (Node.js/Express)
- Create a new file called
server.js
and set up a basic Express server. - Import the necessary dependencies (
express
,html-pdf
) in the server file. - Define routes for generating and downloading invoices.
const express = require('express');
const helmet = require('helmet');
const path = require('path');
const cors = require('cors');
const cookieParser = require('cookie-parser');
require('dotenv').config({ path: '.variables.env' });
const helpers = require('./helpers');
const erpApiRouter = require('./routes/erpRoutes/erpApi');
const erpAuthRouter = require('./routes/erpRoutes/erpAuth');
const erpDownloadRouter = require('./routes/erpRoutes/erpDownloadRouter');
const errorHandlers = require('./handlers/errorHandlers');
const { isValidAdminToken } = require('./controllers/erpControllers/authJwtController');
// create our Express app
const app = express();
// serves up static files from the public folder. Anything in public/ will just be served up as the file it is
// Takes the raw requests and turns them into usable properties on req.body
app.use(helmet());
app.use(cookieParser());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(express.static(path.join(__dirname, 'public')));
// pass variables to our templates + all requests
app.use((req, res, next) => {
res.locals.h = helpers;
res.locals.admin = req.admin || null;
res.locals.currentPath = req.path;
const clientIP = req.socket.remoteAddress;
let isLocalhost = false;
if (clientIP === '127.0.0.1' || clientIP === '::1') {
// Connection is from localhost
isLocalhost = true;
}
res.locals.isLocalhost = isLocalhost;
next();
});
// app.use(function (req, res, next) {
// if (req.url.slice(-1) === "/" && req.path.length > 1) {
// // req.path = req.path.slice(0, -1);
// req.url = req.url.slice(0, -1);
// }
// next();
// });
// Here our API Routes
var corsOptionsDelegate = function (req, callback) {
var corsOptions;
const clientIP = req.socket.remoteAddress;
let isLocalhost = false;
if (clientIP === '127.0.0.1' || clientIP === '::1') {
// Connection is from localhost
isLocalhost = true;
}
if (isLocalhost) {
corsOptions = {
origin: '*',
credentials: true,
};
} else {
corsOptions = {
origin: true,
credentials: true,
};
}
callback(null, corsOptions); // callback expects two parameters: error and options
};
app.use(
'/api',
cors({
origin: true,
credentials: true,
}),
erpAuthRouter
);
app.use(
'/api',
cors({
origin: true,
credentials: true,
}),
isValidAdminToken,
erpApiRouter
);
app.use('/download', cors(), erpDownloadRouter);
// If that above routes didnt work, we 404 them and forward to error handler
app.use(errorHandlers.notFound);
// Otherwise this was a really bad error we didn't expect! Shoot eh
if (app.get('env') === 'development') {
/* Development Error Handler - Prints stack trace */
app.use(errorHandlers.developmentErrors);
}
// production error handler
app.use(errorHandlers.productionErrors);
// done! we export it so we can start the site in start.js
module.exports = app;
Step 3: Building the React.js application
- In the root directory of your project, create a new folder called
client
. - Navigate into the
client
folder and runnpx create-react-app .
to generate a new React.js application. - Replace the contents of the generated
src
folder with your own code. - Create components for the invoice form, invoice list, and invoice detail view.
- Use Redux to manage the state of your application, including the invoice data.
import React from 'react';
import dayjs from 'dayjs';
import { Tag } from 'antd';
import InvoiceModule from '@/modules/InvoiceModule';
import { useMoney } from '@/settings';
export default function Invoice() {
const { moneyRowFormatter } = useMoney();
const entity = 'invoice';
const searchConfig = {
displayLabels: ['name', 'surname'],
searchFields: 'name,surname,birthday',
};
const entityDisplayLabels = ['number', 'client.company'];
const dataTableColumns = [
{
title: '#N',
dataIndex: 'number',
},
{
title: 'Client',
dataIndex: ['client', 'company'],
},
{
title: 'Date',
dataIndex: 'date',
render: (date) => {
return dayjs(date).format('DD/MM/YYYY');
},
},
{
title: 'Due date',
dataIndex: 'expiredDate',
render: (date) => {
return dayjs(date).format('DD/MM/YYYY');
},
},
{
title: 'Total',
dataIndex: 'total',
render: (amount) => moneyRowFormatter({ amount }),
},
{
title: 'Balance',
dataIndex: 'credit',
render: (amount) => moneyRowFormatter({ amount }),
},
{
title: 'status',
dataIndex: 'status',
render: (status) => {
let color = status === 'draft' ? 'cyan' : status === 'sent' ? 'magenta' : 'gold';
return <Tag color={color}>{status && status.toUpperCase()}</Tag>;
},
},
{
title: 'Payment',
dataIndex: 'paymentStatus',
render: (paymentStatus) => {
let color =
paymentStatus === 'unpaid'
? 'volcano'
: paymentStatus === 'paid'
? 'green'
: paymentStatus === 'overdue'
? 'red'
: 'purple';
return <Tag color={color}>{paymentStatus && paymentStatus.toUpperCase()}</Tag>;
},
},
];
const PANEL_TITLE = 'invoice';
const dataTableTitle = 'invoices Lists';
const ADD_NEW_ENTITY = 'Add new invoice';
const DATATABLE_TITLE = 'invoices List';
const ENTITY_NAME = 'invoice';
const CREATE_ENTITY = 'Save invoice';
const UPDATE_ENTITY = 'Update invoice';
const config = {
entity,
PANEL_TITLE,
dataTableTitle,
ENTITY_NAME,
CREATE_ENTITY,
ADD_NEW_ENTITY,
UPDATE_ENTITY,
DATATABLE_TITLE,
dataTableColumns,
searchConfig,
entityDisplayLabels,
};
return <InvoiceModule config={config} />;
}
Step 4: Integrating React.js with Node.js
- In your React.js application, make HTTP requests to the server endpoints created in Step 2 using libraries like
axios
orfetch
. - When submitting the invoice form, send the form data to the server and handle the creation of the PDF invoice on the server side.
- Retrieve the generated PDF from the server and display a link or button to let users download it.
import React, { useState, useEffect } from 'react';
import { Form, Divider } from 'antd';
import { Button, PageHeader, Row, Statistic, Tag } from 'antd';
import { useSelector, useDispatch } from 'react-redux';
import { erp } from '@/redux/erp/actions';
import { selectCreatedItem } from '@/redux/erp/selectors';
import { useErpContext } from '@/context/erp';
import uniqueId from '@/utils/uinqueId';
import Loading from '@/components/Loading';
import { CloseCircleOutlined, PlusOutlined } from '@ant-design/icons';
function SaveForm({ form, config }) {
let { CREATE_ENTITY } = config;
const handelClick = () => {
form.submit();
};
return (
<Button onClick={handelClick} type="primary" icon={<PlusOutlined />}>
{CREATE_ENTITY}
</Button>
);
}
export default function CreateItem({ config, CreateForm }) {
let { entity, CREATE_ENTITY } = config;
const { erpContextAction } = useErpContext();
const { createPanel } = erpContextAction;
const dispatch = useDispatch();
const { isLoading, isSuccess } = useSelector(selectCreatedItem);
const [form] = Form.useForm();
const [subTotal, setSubTotal] = useState(0);
const handelValuesChange = (changedValues, values) => {
const items = values['items'];
let subTotal = 0;
if (items) {
items.map((item) => {
if (item) {
if (item.quantity && item.price) {
let total = item['quantity'] * item['price'];
//sub total
subTotal += total;
}
}
});
setSubTotal(subTotal);
}
};
useEffect(() => {
if (isSuccess) {
form.resetFields();
dispatch(erp.resetAction({ actionType: 'create' }));
setSubTotal(0);
createPanel.close();
dispatch(erp.list({ entity }));
}
}, [isSuccess]);
const onSubmit = (fieldsValue) => {
if (fieldsValue) {
// if (fieldsValue.expiredDate) {
// const newDate = fieldsValue["expiredDate"].format("DD/MM/YYYY");
// fieldsValue = {
// ...fieldsValue,
// expiredDate: newDate,
// };
// }
// if (fieldsValue.date) {
// const newDate = fieldsValue["date"].format("DD/MM/YYYY");
// fieldsValue = {
// ...fieldsValue,
// date: newDate,
// };
// }
if (fieldsValue.items) {
let newList = [...fieldsValue.items];
newList.map((item) => {
item.total = item.quantity * item.price;
});
fieldsValue = {
...fieldsValue,
items: newList,
};
}
}
dispatch(erp.create({ entity, jsonData: fieldsValue }));
};
return (
<>
<PageHeader
onBack={() => createPanel.close()}
title={CREATE_ENTITY}
ghost={false}
tags={<Tag color="volcano">Draft</Tag>}
// subTitle="This is create page"
extra={[
<Button
key={`${uniqueId()}`}
onClick={() => createPanel.close()}
icon={<CloseCircleOutlined />}
>
Cancel
</Button>,
<SaveForm form={form} config={config} key={`${uniqueId()}`} />,
]}
style={{
padding: '20px 0px',
}}
></PageHeader>
<Divider dashed />
<Loading isLoading={isLoading}>
<Form form={form} layout="vertical" onFinish={onSubmit} onValuesChange={handelValuesChange}>
<CreateForm subTotal={subTotal} />
</Form>
</Loading>
</>
);
}
Step 5: Styling and enhancing the user interface
- Utilize CSS and any CSS framework of your choice (e.g., Ant Design) to style your application.
- Enhance the user interface with features like pagination, sorting, searching, and filtering invoices.
import React, { useState, useEffect, useRef } from 'react';
import dayjs from 'dayjs';
import { Form, Input, InputNumber, Button, Select, Divider, Row, Col } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import { DatePicker } from '@/components/CustomAntd';
import AutoCompleteAsync from '@/components/AutoCompleteAsync';
import ItemRow from '@/components/ErpPanel/ItemRow';
import MoneyInputFormItem from '@/components/MoneyInputFormItem';
export default function InvoiceForm({ subTotal = 0, current = null }) {
const [total, setTotal] = useState(0);
const [taxRate, setTaxRate] = useState(0);
const [taxTotal, setTaxTotal] = useState(0);
const [currentYear, setCurrentYear] = useState(() => new Date().getFullYear());
const handelTaxChange = (value) => {
setTaxRate(value);
};
useEffect(() => {
if (current) {
const { taxRate = 0, year } = current;
setTaxRate(taxRate);
setCurrentYear(year);
}
}, [current]);
useEffect(() => {
const currentTotal = subTotal * taxRate + subTotal;
setTaxTotal((subTotal * taxRate).toFixed(2));
setTotal(currentTotal.toFixed(2));
}, [subTotal, taxRate]);
const addField = useRef(false);
useEffect(() => {
addField.current.click();
}, []);
return (
<>
<Row gutter={[12, 0]}>
<Col className="gutter-row" span={9}>
<Form.Item
name="client"
label="Client"
rules={[
{
required: true,
message: 'Please input your client!',
},
]}
>
<AutoCompleteAsync
entity={'client'}
displayLabels={['company']}
searchFields={'company,managerSurname,managerName'}
// onUpdateValue={autoCompleteUpdate}
/>
</Form.Item>
</Col>
<Col className="gutter-row" span={5}>
<Form.Item
label="Number"
name="number"
initialValue={1}
rules={[
{
required: true,
message: 'Please input invoice number!',
},
]}
>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col className="gutter-row" span={5}>
<Form.Item
label="year"
name="year"
initialValue={currentYear}
rules={[
{
required: true,
message: 'Please input invoice year!',
},
]}
>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
</Col>
<Col className="gutter-row" span={5}>
<Form.Item
label="status"
name="status"
rules={[
{
required: false,
message: 'Please input invoice status!',
},
]}
initialValue={'draft'}
>
<Select
options={[
{ value: 'draft', label: 'Draft' },
{ value: 'pending', label: 'Pending' },
{ value: 'sent', label: 'Sent' },
]}
></Select>
</Form.Item>
</Col>
<Col className="gutter-row" span={9}>
<Form.Item label="Note" name="note">
<Input />
</Form.Item>
</Col>
<Col className="gutter-row" span={8}>
<Form.Item
name="date"
label="Date"
rules={[
{
required: true,
type: 'object',
},
]}
initialValue={dayjs()}
>
<DatePicker style={{ width: '100%' }} format={'DD/MM/YYYY'} />
</Form.Item>
</Col>
<Col className="gutter-row" span={7}>
<Form.Item
name="expiredDate"
label="Expire Date"
rules={[
{
required: true,
type: 'object',
},
]}
initialValue={dayjs().add(30, 'days')}
>
<DatePicker style={{ width: '100%' }} format={'DD/MM/YYYY'} />
</Form.Item>
</Col>
</Row>
<Divider dashed />
<Row gutter={[12, 12]} style={{ position: 'relative' }}>
<Col className="gutter-row" span={5}>
<p>Item</p>
</Col>
<Col className="gutter-row" span={7}>
<p>Description</p>
</Col>
<Col className="gutter-row" span={3}>
<p>Quantity</p>
</Col>
<Col className="gutter-row" span={4}>
<p>Price</p>
</Col>
<Col className="gutter-row" span={5}>
<p>Total</p>
</Col>
</Row>
<Form.List name="items">
{(fields, { add, remove }) => (
<>
{fields.map((field) => (
<ItemRow key={field.key} remove={remove} field={field} current={current}></ItemRow>
))}
<Form.Item>
<Button
type="dashed"
onClick={() => add()}
block
icon={<PlusOutlined />}
ref={addField}
>
Add field
</Button>
</Form.Item>
</>
)}
</Form.List>
<Divider dashed />
<div style={{ position: 'relative', width: ' 100%', float: 'right' }}>
<Row gutter={[12, -5]}>
<Col className="gutter-row" span={5}>
<Form.Item>
<Button type="primary" htmlType="submit" icon={<PlusOutlined />} block>
Save Invoice
</Button>
</Form.Item>
</Col>
<Col className="gutter-row" span={4} offset={10}>
<p
style={{
paddingLeft: '12px',
paddingTop: '5px',
}}
>
Sub Total :
</p>
</Col>
<Col className="gutter-row" span={5}>
<MoneyInputFormItem readOnly value={subTotal} />
</Col>
</Row>
<Row gutter={[12, -5]}>
<Col className="gutter-row" span={4} offset={15}>
<Form.Item
name="taxRate"
rules={[
{
required: false,
message: 'Please input your taxRate!',
},
]}
initialValue="0"
>
<Select
value={taxRate}
onChange={handelTaxChange}
bordered={false}
options={[
{ value: 0, label: 'Tax 0 %' },
{ value: 0.19, label: 'Tax 19 %' },
]}
></Select>
</Form.Item>
</Col>
<Col className="gutter-row" span={5}>
<MoneyInputFormItem readOnly value={taxTotal} />
</Col>
</Row>
<Row gutter={[12, -5]}>
<Col className="gutter-row" span={4} offset={15}>
<p
style={{
paddingLeft: '12px',
paddingTop: '5px',
}}
>
Total :
</p>
</Col>
<Col className="gutter-row" span={5}>
<MoneyInputFormItem readOnly value={total} />
</Col>
</Row>
</div>
</>
);
}
Step 6: Testing and debugging
- Use tools like React DevTools and Redux DevTools to debug your application.
- Write unit tests using libraries like Jest or Enzyme to ensure the stability of your codebase.
Step 7: Deployment
- Deploy your Node.js server and React.js application to a hosting platform like Heroku, AWS, or Netlify.
- Configure the necessary environment variables and ensure that everything is working as expected in a production environment.
Github Repository : https://github.com/idurar/idurar-erp-crm
This tutorial provides a high-level overview of building and generate Invoice PDF system using React.js, Redux, and Node.js. Good luck with your project!
Top comments (10)
Good Work
Thank you Robina, Join us in contributing to the IDURAR open-source project based on Node.js and React.js. We have a collection of "Good First Issue" tickets specially curated for new contributors like you.
we invite you to pick one of new good issue for new Contributors : github.com/idurar/idurar-erp-crm/i...
could i contribute in your project?
Clear and simple ! good job
Well explained
good work
Well Explained hey !
Nice
Good sir
Good Work! Could you provide the generated Invoice PDF? I'm a open-source beginner and I'd like to join the open-source project. This is my Github profile: github.com/ltyzzzxxx 🤩