Building an 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:
You can find the Github Repository for this project here: https://github.com/idurar/idurar-erp-crm
npm init
.npm install react redux react-redux
.
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;
client
.client
folder and run npx create-react-app .
to generate a new React.js application.src
folder with your own code.
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} />;
}
axios
or fetch
.
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>
</>
);
}
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>
</>
);
}
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.