DEV Community

Cover image for ๐Ÿš€ Building and Generate Invoice PDF with Node.js React.js
Salah Eddine Lalami for IDURAR | Where Ai Build Software

Posted on • Updated on • Originally published at idurarapp.com

๐Ÿš€ Building and Generate Invoice PDF with Node.js React.js

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

Open Source Invoice

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;

Enter fullscreen mode Exit fullscreen mode

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 run npx 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} />;
}

Enter fullscreen mode Exit fullscreen mode

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 or 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>
    </>
  );
}


Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}

Enter fullscreen mode Exit fullscreen mode

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

Open Source Invoice

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 (12)

Collapse
 
robinamirbahar profile image
Robina

Good Work

Collapse
 
lalami profile image
Salah Eddine Lalami

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...

Collapse
 
jobayermannan profile image
Jobayer Mannan

could i contribute in your project?

Thread Thread
 
lalami profile image
Salah Eddine Lalami

yes of course , here some first good issue : github.com/idurar/idurar-erp-crm/i...

Collapse
 
fernando_rodrigues profile image
Fernando

Well explained

Collapse
 
hssanbzlm profile image
Hssan Bouzlima

Clear and simple ! good job

Collapse
 
abhijitez profile image
Abhijit Ezhava

Nice

Collapse
 
eldiablo profile image
yamba

good work

Collapse
 
respect17 profile image
Kudzai Murimi

Well Explained hey !

Collapse
 
gopalece1986 profile image
gopalece1986

Good sir

Collapse
 
ltyzzz profile image
Tycho

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 ๐Ÿคฉ

Collapse
 
lalami profile image
Salah Eddine Lalami

Hi @ltyzzz you are welcome to contribute