Building an Invoice PDF system with , , and can be a complex task, but I'm here to guide you through the process. React.js Redux Node.js Here's a step-by-step tutorial on how you can create such a system: You can find the Github Repository for this project here: https://github.com/idurar/idurar-erp-crm Content Overview Step 1: Setting up the environment Step 2: Setting up the server (Node.js/Express) Step 3: Building the React.js application Step 4: Integrating React.js with Node.js Step 5: Styling and enhancing the user interface Step 6: Testing and debugging Step 7: Deployment Step 1: Setting 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 and set up a basic Express server. server.js Import the necessary dependencies ( , ) in the server file. express html-pdf 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 folder and run to generate a new React.js application. client npx create-react-app . Replace the contents of the generated folder with your own code. src 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 or . axios fetch 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. You can find the Github Repository here: https://github.com/idurar/idurar-erp-crm This tutorial provides a high-level overview of building an Invoice PDF system using React.js, Redux, and Node.js. Good luck with your project! Also published . here