DEV Community

Cover image for How to Build an Invoicing App with Next.js, Strapi, and jspdf
Juliet Ofoegbu for Strapi

Posted on

How to Build an Invoicing App with Next.js, Strapi, and jspdf

Introduction

In this tutorial, you'll learn how to build an invoicing app using Next.js and Strapi as the backend and content management. This tutorial is a detailed guide on how to create and set up a Strapi project, how to create Strapi collections, and how to connect the Strapi backend to the Next.js frontend to build a functional invoicing app with CRUD functionalities.

An invoice is a document issued by a seller or service provider to a buyer or client requesting payment for goods bought or services rendered. The invoice includes information such as the items purchased or services provided, payment information, amount of goods, agreed-upon rate, total price of good/services, shipping address, and so on.

An invoicing app is an application or software that allows you to create/generate invoices that can be downloaded in any format and emailed to a client. It is usually designed as a template so that users do not have to create invoice layouts from scratch, and it includes all transaction details between a buyer/client and a seller.

Prerequisites

  • Ensure to have NodeJs installed on your local machine.
  • Basic understanding of Next.js.
  • Understanding of CRUD operations.
  • Experience with RESTful APIs.

Project Overview

The invoicing app we'll be working on will allow users to generate or add invoices that will be sent to the Strapi backend, as well as fetch, update, and delete invoices.

These features will be created with Next.js for the frontend UI and logic, Strapi CMS for invoice storage, and the jsPDF library to make invoices downloadable.

Here's an image of what we're going to build:

Image description

Setting up Strapi Backend

In this section, we'll set up Strapi as the backend platform to store the invoice data.

Create Project Directory

Create a directory for your project. This directory will contain the Strapi backend folder and the Next.js frontend folder for the project.

mkdir invoicing-app && cd invoicing-app
Enter fullscreen mode Exit fullscreen mode

Create a Strapi Project

Next is to create a new Strapi project and you can do this using this command:

npx create-strapi-app@latest backend --quickstart`
Enter fullscreen mode Exit fullscreen mode

This will create a new Strapi application in the invoicing-app directory and install necessary dependencies like Strapi plugins.

After a successful installation, your default browser automatically opens up a new tab for the Strapi admin panel at "http://localhost:1337/admin". If it doesn't, just copy the link provided in the terminal and paste in your browser. Fill in your details on the form provided and click on the "Let's start" button.

Your Strapi admin dashboard is ready for use!

Image description

Create Collection Type and Content Types

If you click on the 'Content-Type Builder' tab at the left sidebar, you'll see that there's already a collection type named 'User'. Create a new collection type by clicking on the "+ Create new collection type".

A modal box with a form will open up. For the 'Display name' field, enter 'Invoices'. The API ID (Singular) and API ID (Plular) will be automatically generated.

Image description

When you click the "continue" button, you will be taken to the next page where you'll have to select the fields for your content type. For this project, these are the fields you'll need:

Field Name Data Type
name Text - Short text
senderEmail Email
recipientEmail Email
date Date
dueDate Date
shippingAddress Text - Long text
invoiceNote Text - Long text
description Text - Short text
qty Number
rate Number
total Number

Click the 'Finish' button and your invoice collection type should now look like this:

Image description

Save this action by clicking on the 'Save' button located at the top right-hand corner of the screen. This will restart the server so wait for it to reload.

Create Entries

To test this, you can add an entry for this collection. Click on the 'Content Manager' at the sidebar and then go to 'Invoices'. Click the "+ Create new entry" button at the top right corner of the screen.

Image description

Fill in some details in the fields. Save it and then hit the 'Publish' button to add the entry.

NOTE: They are saved as drafts by default, so you need to publish it to view it.

Enable Public API Access

The last set up here is to grant permission for user to create, find, edit, and delete invoices in the app. To do this, go to Settings on the side panel, click on Roles under the USERS & PERMISSIONS PLUGIN section. Select Public.

Toggle the 'Invoices' section and then check the 'Select all' checkbox. This will allow access to all CRUD operations. Save it.

Image description

Setting up the Next.js project

Here, we'll set up the Next.js project to build the frontend view that will allow users to view fetched invoices, create invoices, edit invoices, and delete invoices.

Create a Next.js App

Go to your root directory /invoicing-app. Create your Next.js project using this command:

npx create-next-app frontend
Enter fullscreen mode Exit fullscreen mode

When prompted in the command line, choose to use 'Typescript' for the project. Select 'Yes' for the ESLint, and 'Yes' to use the src/ directory, 'Yes' for the experimental app directory to complete the set up. This will create a new Next.js app.

Navigate to the app you just created using the command below:

cd frontend
Enter fullscreen mode Exit fullscreen mode

Install Dependencies

Next install the necessary dependencies for this project :

  1. Axios: Axios is a JavaScript library used to send asynchronous HTTP requests to REST endpoints. It's commonly used to perform CRUD operations. Install Axios using this command:
npm install axios
Enter fullscreen mode Exit fullscreen mode
  1. jsPDF: We want users to be able to download any invoice they create in a PDF format. Now instead of using the regular window print method, let's use this JavaScript jsPDF library which is customizable. It's a library used for generating PDFs in JavaScript. With jsPDF, you can format and customize the layout of your generated PDF.

  2. jspdf-autotable: We'll use jsPDF along with the jspdf-autotable, a jsPDF plugin for generating tables. This jsPDF plugin adds the ability to generate PDF tables either by parsing HTML tables or by using Javascript data directly.

Install these libraries using this command:

npm i jspdf jspdf-autotable
Enter fullscreen mode Exit fullscreen mode

Start up your frontend app with the following command:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Access it on your browswer using the link "http://localhost:3000".

Project folder structure

  • For this app, we'll need 3 files namely (pages.tsx, Invoices.tsx, and InvoiceForm.tsx) to make this app work. If you want to style your app with regular CSS, you can make changes to your CSS files. In this article, you will use TailwindCSS to style your application.

  • Create a new folder inside the src folder called components. Create 2 components or files inside this folder and name them: Invoices.tsx and InvoiceForm.tsx.
    The Invoices component will be where all the invoices created will be displayed. It will also be the main page of the application. The InvoiceForm component is for the form modal where users will have to input details to create or edit an invoice.

  • In the app directory, locate pages.tsx and replace the code with these lines of code:

'use client'
import Invoices from "../components/Invoices";

function App() {
    return (
        <div className="p-5">
            <Invoices />
        </div>
    );
}
export default App;
Enter fullscreen mode Exit fullscreen mode

The main component which is the Invoices.tsx component is imported and rendered as the main page of the application.

Building the components and adding CRUD functionalities

Here, we'll build the app's components and add the CRUD functionalities to enable users fetch invoices from the Strapi backend, create new invoices, edit invoices, and delete invoices.

Create the Invoice Form

In your InvoiceForm.tsx component, paste these lines of code:

"use client";
import React, { ChangeEvent, useEffect, useReducer, useState } from "react";
import axios from "axios";

interface InvoiceFormProps {
  onClose: () => void;
  setInvoices: React.Dispatch<React.SetStateAction<Invoice[]>>;
  selectedInvoice: Invoice | null;
}

interface Invoice {
  id: number;
  name: string;
  attributes: {};
  senderEmail: string;
  recipientEmail: string;
  shippingAddress: string;
  date: string;
  dueDate: string;
  invoiceNote: string;
  description: string;
  qty: number;
  rate: number;
  total: number;
}

const InvoiceForm: React.FC<InvoiceFormProps> = ({
  onClose,
  setInvoices,
  selectedInvoice,
}) => {
  const initialState = {
    name: "",
    senderEmail: "",
    recipientEmail: "",
    shippingAddress: "",
    date: "",
    dueDate: "",
    invoiceNote: "",
    description: "",
    qty: 0,
    rate: 0,
    total: 0,
  };

  function reducer(
    state = initialState,
    { field, value }: { field: string; value: any },
  ) {
    return { ...state, [field]: value };
  }

  const [formFields, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    if (selectedInvoice) {
      for (const [key, value] of Object.entries(selectedInvoice?.attributes)) {
        dispatch({ field: key, value });
      }
    } else {
      for (const [key, value] of Object.entries(initialState)) {
        dispatch({ field: key, value });
      }
    }
  }, [selectedInvoice]);

  const handleInputChange = (
    e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>,
  ) => {
    const { name, value } = e.target;
    dispatch({ field: name, value });
  };

  useEffect(() => {
    const { qty, rate } = formFields;
    const total = qty * rate;
    dispatch({ field: "total", value: total });
  }, [formFields.qty, formFields.rate]);

  const handleSendInvoice = async () => {
    try {
      const {
        name,
        senderEmail,
        recipientEmail,
        date,
        dueDate,
        shippingAddress,
        invoiceNote,
        description,
        qty,
        rate,
        total,
      } = formFields;

      if (selectedInvoice) {
        // Update an existing invoice
        const data = await axios.put(
          `http://localhost:1337/api/invoices/${selectedInvoice.id}`,
          {
            data: {
              name,
              senderEmail,
              recipientEmail,
              shippingAddress,
              dueDate,
              date,
              invoiceNote,
              description,
              qty,
              rate,
              total,
            },
          },
        );
        console.log(data);
        setInvoices((prev) =>
          prev.map((inv) =>
            inv.id === selectedInvoice.id ? { ...inv, ...formFields } : inv,
          ),
        );
        window.location.reload();
      } else {
        // Create a new invoice
        const { data } = await axios.post(
          "http://localhost:1337/api/invoices",
          {
            data: {
              name,
              senderEmail,
              recipientEmail,
              shippingAddress,
              dueDate,
              date,
              invoiceNote,
              description,
              qty,
              rate,
              total,
            },
          },
        );
        console.log(data);
        setInvoices((prev) => [...prev, data.data]);
      }

      onClose();
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <>
      <main className="fixed top-0 z-50 left-0 w-screen h-screen flex justify-center items-center bg-black bg-opacity-50">
        <section className="relative lg:px-10 px-6 py-8 lg:mt-8 lg:w-[60%] bg-white shadow-md rounded px-8 pt-2 pb-8 mb-4">
          <form className="pt-4">
            <h2 className="text-lg font-medium mb-4">
              {selectedInvoice ? "Edit Invoice" : "Create Invoice"}
            </h2>
            <button
              className="absolute top-2 right-8 font-bold text-black cursor-pointer text-2xl"
              onClick={onClose}
            >
              &times;
            </button>

            <div className="mb-4 flex flex-row justify-between">
              <div className="flex flex-col w-[30%]">
                <label
                  className="block text-gray-700 text-sm font-bold mb-2"
                  htmlFor="name"
                >
                  Your name
                </label>
                <input
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                  id="name"
                  name="name"
                  type="text"
                  placeholder="Sender's name"
                  onChange={handleInputChange}
                  value={formFields.name}
                  required
                />
              </div>

              <div className="flex flex-col w-[30%]">
                <label
                  className="block text-gray-700 text-sm font-bold mb-2"
                  htmlFor="senderEmail"
                >
                  Your email address
                </label>
                <input
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                  id="senderEmail"
                  name="senderEmail"
                  type="email"
                  placeholder="Sender's email"
                  onChange={handleInputChange}
                  value={formFields.senderEmail}
                  required
                />
              </div>

              <div className="flex flex-col w-[30%]">
                <label
                  className="block text-gray-700 text-sm font-bold mb-2"
                  htmlFor="recipientEmail"
                >
                  Recipient's Email
                </label>
                <input
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
                  id="recipientEmail"
                  name="recipientEmail"
                  type="email"
                  placeholder="Client's email address"
                  onChange={handleInputChange}
                  value={formFields.recipientEmail}
                  required
                />
              </div>
            </div>

            <div className="mb-4 flex flex-row justify-between">
              <div className="flex flex-col w-[45%]">
                <label
                  className="block text-gray-700 text-sm font-bold mb-2"
                  htmlFor="date"
                >
                  Date
                </label>
                <input
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
                  id="date"
                  name="date"
                  type="date"
                  onChange={handleInputChange}
                  value={formFields.date}
                  required
                />
              </div>

              <div className="flex flex-col w-[45%]">
                <label
                  className="block text-gray-700 text-sm font-bold mb-2"
                  htmlFor="dueDate"
                >
                  Due Date
                </label>
                <input
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
                  id="dueDate"
                  name="dueDate"
                  type="date"
                  onChange={handleInputChange}
                  value={formFields.dueDate}
                  required
                />
              </div>
            </div>

            <div className="mb-4 flex flex-row justify-between">
              <div className="flex flex-col w-[45%]">
                <label
                  className="block text-gray-700 text-sm font-bold mb-2"
                  htmlFor="shippingAddress"
                >
                  Shipping Address
                </label>
                <textarea
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
                  id="shippingAddress"
                  name="shippingAddress"
                  placeholder="Office address of recipient"
                  onChange={handleInputChange}
                  value={formFields.shippingAddress}
                  required
                />
              </div>

              <div className="flex flex-col w-[45%]">
                <label
                  htmlFor="invoiceNote"
                  className="block text-gray-700 text-sm font-bold mb-2 w-full"
                >
                  Invoice Note
                </label>
                <textarea
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
                  id="invoiceNote"
                  name="invoiceNote"
                  placeholder="Account details"
                  onChange={handleInputChange}
                  value={formFields.invoiceNote}
                  required
                />
              </div>
            </div>

            <div className="flex justify-center items-center">
              <label
                htmlFor="description"
                className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
              >
                Invoice Item
                <input
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
                  id="description"
                  name="description"
                  type="text"
                  placeholder="Reason for invoice"
                  onChange={handleInputChange}
                  value={formFields.description}
                  required
                />
              </label>

              <label
                htmlFor="qty"
                className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
              >
                Quantity
                <input
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
                  id="qty"
                  name="qty"
                  type="number"
                  onChange={handleInputChange}
                  value={formFields.qty}
                  required
                />
              </label>

              <label
                htmlFor="rate"
                className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5"
              >
                Rate
                <input
                  className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight focus:outline-none focus:shadow-outline"
                  id="rate"
                  name="rate"
                  type="number"
                  onChange={handleInputChange}
                  value={formFields.rate}
                  required
                />
              </label>

              <div className="block text-gray-700 text-sm font-bold mb-2 w-full mr-5">
                <label>Total</label>
                <div className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 mb-3 leading-tight">
                  {formFields.total}
                </div>
              </div>
            </div>

            <hr className="mt-5 border-1" />

            <div className="mt-4 flex justify-center">
              <button
                type="button"
                className="py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
                onClick={handleSendInvoice}
              >
                {selectedInvoice ? "Update Invoice" : "Send Invoice"}
              </button>
            </div>
          </form>
        </section>
      </main>
    </>
  );
};

export default InvoiceForm;

Enter fullscreen mode Exit fullscreen mode

Code explanation

  • After importing the necessary libraries, we defined the InvoiceFormProps interface by passing in three props. The first one onClose is the function to close the form. The second prop setInvoices is a function to update the invoices state in the parent component. The third one selectedInvoice is the invoice being edited if any has been selected.

  • We then defined Invoice interfaces to type-check the props and state used in the component.

  • The initialState object defines the initial state for the form fields. The reducer function updates the state based on the field and value provided to handle form field updates. The next line uses useReducer hook to manage form fields state, initializing with initialState.

  • The first useEffect function is for pre-filling the form when user wants to edit the invoice. It runs whenever the selectedInvoice changes. If there is a selectedInvoice (indicating the user is editing an existing invoice), it populates the form fields with the invoice data. If there is no selected invoice, it resets the form fields to their initial values.
    The [selectedInvoice] dependency array ensures this effect runs only when selectedInvoice changes.

  • The second useEffect function is for calculating the total amount of the invoice, so that the total value of an invoice will be the quantity of the item multiplied by the rate being charged. This effect calculates the total whenever the qty or rate changes.
    It extracts qty and rate from formFields, calculates the total by multiplying them, and then dispatches an action to update the total field in the state with the calculated value.

  • The handleInputChange function handles changes to the form input fields and updates the corresponding state fields.
    It destructures name and value from the event target and then dispatches an action to update the state field corresponding to name with the new value.

  • The handleSendInvoice funtion handles the logic for sending an invoice to the Strapi backend. It sends a POST request to create a new invoice or a PUT request to update an existing one. It first extracts the necessary invoice details from formFields and then checks if selectedInvoice exists.

  • If it exists, it means the user is updating an existing invoice. So it sends a PUT request to update the existing invoice on the server. It also updates the local state with the modified invoice data.

  • If it does not exist, it means the user is creating a new invoice. So it sends a POST request to create a new invoice on the server. It then adds the newly created invoice to the local state.

  • The onClose function is called to close the form whenever a user submits the form and the error handling to catch any errors during the request and log them to the console.

  • The JSX for the invoice form is rendered. The form has a header that dynamically displays "Edit Invoice" or "Create Invoice" based on whether selectedInvoice is active.
    The form is displayed with fields for the sender's name, email, recipient's email, dates, shipping address, invoice note, item description, quantity, rate, and total.
    A button is provided to send the invoice, which triggers the handleSendInvoice function.

Display Invoices

This component is responsible for displaying the invoice and its contents. When users retrieve invoices from Strapi, they will be displayed here, along with buttons for generating, updating, deleting, and downloading them. The form for creating an invoice will also be displayed on this page.

In your Invoices.tsx component, paste these lines of code:

import React, { useEffect, useState } from "react";
import axios from "axios";
import InvoiceForm from "./InvoiceForm";

interface Invoice {
  [x: string]: any;
  id: number;
  name: string;
  senderEmail: string;
  recipientEmail: string;
  date: string;
  dueDate: string;
  shippingAddress: string;
  invoiceNote: string;
  description: string;
  qty: number;
  rate: number;
  total: number;
}

const Invoices: React.FC = () => {
  const [invoices, setInvoices] = useState<Invoice[]>([]);
  const [isInvoiceFormOpen, setIsInvoiceFormOpen] = useState(false);
  const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null);

  useEffect(() => {
    const fetchInvoices = () => {
      fetch("http://localhost:1337/api/invoices?populate=invoice")
        .then((res) => {
          if (!res.ok) {
            throw new Error("Network response was not ok");
          }
          return res.json();
        })
        .then((data) => {
          console.log("Fetched invoices:", data);
          if (Array.isArray(data.data)) {
            setInvoices(data.data);
          } else {
            console.error("Fetched data is not an array");
          }
        })
        .catch((error) => {
          console.error("Error fetching invoices:", error);
        });
    };

    fetchInvoices();
  }, []);

  const handleOpenInvoiceForm = () => {
    setSelectedInvoice(null);
    setIsInvoiceFormOpen(true);
  };

  const handleCloseInvoiceForm = () => {
    setSelectedInvoice(null);
    setIsInvoiceFormOpen(false);
  };

  const handleEditInvoice = (invoice: Invoice) => {
    console.log("Invoice being edited:", invoice);
    setSelectedInvoice(invoice);
    setIsInvoiceFormOpen(true);
  };

  const handleDeleteInvoice = async (id: number) => {
    try {
      alert("Are you sure you want to delete this invoice?");
      await axios.delete(`http://localhost:1337/api/invoices/${id}`);
      setInvoices(invoices.filter((invoice) => invoice.id !== id));
    } catch (error) {
      console.error(error);
    }
  };

  return (
    <div className="flex flex-col items-center justify-center">
      <section className="w-[65%] flex flex-row justify-between py-4">
        <h2 className="text-3xl text-gray-700 font-medium">INVOICE</h2>
        <button
          onClick={handleOpenInvoiceForm}
          className="bg-green-500 p-2 w-30 text-white rounded-lg"
        >
          Create invoice
        </button>
      </section>

      {isInvoiceFormOpen && (
        <InvoiceForm
          onClose={handleCloseInvoiceForm}
          setInvoices={setInvoices}
          selectedInvoice={selectedInvoice}
        />
      )}

      {invoices.length === 0 ? (
        <p>No invoice yet.</p>
      ) : (
        <div className="w-[70%]">
          <div className="px-5 py-5 mx-auto">
            {invoices.map((invoice) => (
              <>
                <div
                  className="flex flex-wrap border-t-2 border-b-2 border-gray-200 border-opacity-60"
                  key={invoice.id}
                >
                  <div className="lg:w-1/3 md:w-full px-8 py-6 border-opacity-60">
                    <div>
                      <h2 className="text-base text-gray-900 font-medium mb-1">
                        Issued:
                      </h2>
                      <p className="leading-relaxed text-sm mb-4">
                        {invoice.attributes.date}
                      </p>
                    </div>
                    <div className="mt-12">
                      <h2 className="text-base text-gray-900 font-medium">
                        Due:
                      </h2>
                      <p className="leading-relaxed text-sm mb-4">
                        {invoice.attributes.dueDate}
                      </p>
                    </div>
                  </div>

                  <div className="lg:w-1/3 md:w-full px-8 py-6 border-l-2 border-gray-200 border-opacity-60">
                    <h2 className="text-base text-gray-900 font-medium mb-2">
                      Billed To:
                    </h2>
                    <div className="">
                      <h2 className=" text-gray-900 text-sm mb-1 font-medium">
                        Recipient's Email
                      </h2>
                      <p className="leading-relaxed text-sm mb-5">
                        {invoice.attributes.recipientEmail}
                      </p>
                    </div>

                    <div>
                      <h2 className=" text-gray-900 text-sm mb-1 font-medium">
                        Shipping Address
                      </h2>
                      <p className="leading-relaxed text-sm mb-4">
                        {invoice.attributes.shippingAddress}
                      </p>
                    </div>
                  </div>

                  <div className="lg:w-1/3 md:w-full px-8 py-6 border-l-2 border-gray-200 border-opacity-60">
                    <h2 className="text-base text-gray-900 font-medium mb-2">
                      From:
                    </h2>
                    <div className="">
                      <h2 className=" text-gray-900 text-sm mb-1 font-medium">
                        Sender's Name
                      </h2>
                      <p className="leading-relaxed text-sm mb-5">
                        {invoice.attributes.name}
                      </p>
                    </div>

                    <div>
                      <h2 className=" text-gray-900 text-sm mb-1 font-medium">
                        Sender's Email
                      </h2>
                      <p className="leading-relaxed text-sm mb-4">
                        {invoice.attributes.senderEmail}
                      </p>
                    </div>
                  </div>
                </div>

                <div className="w-full px-5 py-12 mx-auto">
                  <div className="flex flex-row justify-between border-b-2 border-gray-300">
                    <div>
                      <h2 className="text-lg font-medium text-gray-700 mb-2">
                        Invoice Item
                      </h2>
                    </div>

                    <div className="flex flex-row mb-2">
                      <p className="ml-2 text-lg font-medium text-gray-800">
                        Qty
                      </p>
                      <p className="ml-[6rem] text-lg font-medium text-gray-800">
                        Rate
                      </p>
                      <p className="ml-[6rem] text-lg font-medium text-gray-800">
                        Total
                      </p>
                    </div>
                  </div>

                  <div className="flex flex-row justify-between mt-4">
                    <div>
                      <h2 className="text-base text-gray-700 mb-4">
                        {invoice.attributes.description}
                      </h2>
                    </div>

                    <div className="flex flex-row mb-4">
                      <p className="ml-2 text-base text-gray-800">
                        {invoice.attributes.qty}
                      </p>
                      <p className="ml-[6rem] text-base text-gray-800">
                        ${invoice.attributes.rate}
                      </p>
                      <p className="ml-[6rem] text-base text-gray-800">
                        ${invoice.attributes.total}
                      </p>
                    </div>
                  </div>

                  <div className="grid justify-end pt-[2.5rem]">
                    <div className="flex flex-row justify-between">
                      <div>
                        <h2 className="text-lg font-medium text-gray-700 mb-4">
                          Tax (0%)
                        </h2>
                      </div>

                      <div className="flex flex-row">
                        <p className="ml-[10rem] text-base text-gray-800">
                          0.00
                        </p>
                      </div>
                    </div>

                    <div className="flex flex-row justify-between border-y-2 border-green-400">
                      <div className="pt-4">
                        <h2 className="text-lg font-medium text-gray-700 mb-4">
                          Amount due:
                        </h2>
                      </div>

                      <div className="flex flex-row pt-4">
                        <p className="ml-[10rem] text-lg font-medium text-gray-800">
                          ${invoice.attributes.total}.00
                        </p>
                      </div>
                    </div>
                  </div>
                </div>

                <div className="flex flex-row justify-between w-full mt-1">
                  <div>
                    <button className="bg-blue-500 px-2 py-2 rounded text-white hover:bg-blue-600">
                      Download invoice
                    </button>

                    <button
                      className="bg-green-500 px-2 py-2 rounded text-white hover:bg-green-600 ml-4"
                      onClick={() => handleEditInvoice(invoice)}
                    >
                      Edit invoice
                    </button>
                  </div>

                  <div className="flex justify-end bg-red-400 px-2 py-2 rounded text-white hover:bg-red-500">
                    <button onClick={() => handleDeleteInvoice(invoice.id)}>
                      Delete invoice
                    </button>
                  </div>
                </div>
              </>
            ))}
          </div>
        </div>
      )}
    </div>
  );
};

export default Invoices;

Enter fullscreen mode Exit fullscreen mode

Code explanation

  • First, we imported the InvoiceForm component which will be used here, along with the libraries installed.

  • Since we're working with TypeScript, we set TypeScript interface to define the structure of the invoice object.

  • We then set three states. The first state const [invoices, setInvoices] = useState<Invoice[]>([]); is an array to store fetched invoices. The second state const [isInvoiceFormOpen, setIsInvoiceFormOpen] = useState(false); will manage the visibility of the InvoiceForm. The third state const [selectedInvoice, setSelectedInvoice] = useState<Invoice | null>(null); will store the invoice currently being edited.

  • The useEffect hook is used to fetch invoices from the Strapi backend when the component mounts using the fetch API. The fetched data is stored in the invoices state.

  • If the response is not OK, an error is thrown. If it's ok, the response is parsed as JSON. We also set a condition to check if the fetched invoices is an array so we'd be able to map through it to display the invoices. If is an array, it sets this data in the invoices state. If not, it logs an error.

  • The handleCloseInvoiceForm and handleCloseInvoiceForm functions handle the opening and closing of the form modal.

  • We defined the handleEditInvoice function that opens the invoice form pre-populated with the selected invoice's details for editing. It sets the selectedInvoice to the invoice to be edited and opens the invoice form by setting isInvoiceFormOpen to true.

  • Next is the handleDeleteInvoice function that deletes an invoice by selecting its id and sending a DELETE request to the API. This removes or filters out the deleted invoice from the invoices state, and logs any error that will occur during the request.

  • The component renders a list of invoices by mapping through the invoices array and rendering each invoice with a unique key. Each invoice displays details and buttons for editing and deleting.
    If the isInvoiceFormOpen state is true, the InvoiceForm component is rendered for creating or editing invoices.

  • This JSX will also conditionally render a message if there are no invoices to be displayed, otherwise renders the list of invoices. This is added so that the page doesn't look blank when there are no invoices to be displayed.

Adding the jsPDF Download Functionality

To enable users download any created invoice in PDF format, let's utilize the jsPDF library. We'll also customize the PDF format a bit.

In your Invoices.tsx component:

Step 1: Import jsPDF and autotable Plugin

Import the jsPDF library and the autotable plugin in the component:

import jsPDF from 'jspdf';
import 'jspdf-autotable';
Enter fullscreen mode Exit fullscreen mode

Step 2: Allow Generating Tables in PDFs

The custom class PDFWithAutoTable is included to extend jsPDF to include the autoTable method for generating tables in PDFs.

class PDFWithAutoTable extends jsPDF {
  autoTable(options: any) {
    // @ts-ignore
    super.autoTable(options);
  }
}

Enter fullscreen mode Exit fullscreen mode

Step 3: Handle Invoice Download

The last step is to create a function to handle invoice download and add this function to the "Download invoice" button in the JSX.
The handleDownloadPDF function initializes a new PDFWithAutoTable document, sets the font size and style. It sets the invoice data to be included in the table 'const tableData[{ }]'. It then uses autoTable property from the jspdf-autotable library to add the data to the PDF in a table format.

The last line saves the generated PDF with a filename that includes the invoice ID for easy identification.

const handleDownloadPDF = (invoice: Invoice) => {
  const doc = new PDFWithAutoTable();

  // Set the font size and style
  doc.setFontSize(12);
  doc.setFont("helvetica", "normal");

  // Tabular format of the invoice with corresponding information
  const tableData = [
    ["Invoice id", `${invoice.id}`],
    ["Sender's name", `${invoice.attributes.name}`],
    ["Sender's email", `${invoice.attributes.senderEmail}`],
    ["Recipient's email", `${invoice.attributes.recipientEmail}`],
    ["Invoice date", `${invoice.attributes.date}`],
    ["Due date", `${invoice.attributes.dueDate}`],
    ["Shipping address", `${invoice.attributes.shippingAddress}`],
    ["Invoice note", `${invoice.attributes.invoiceNote}`],
    ["Invoice description", `${invoice.attributes.description}`],
    ["Item quantity", `(${invoice.attributes.qty})`],
    ["Rate", `${invoice.attributes.rate}`],
    ["Total", `${invoice.attributes.total}`],
  ];

  // Customizing the table
  doc.autoTable({
    startY: 40,
    head: [["Item", "Details"]],
    body: tableData,
    headStyles: { fontSize: 18, fontStyle: "bold" },
    styles: { fontSize: 15, fontStyle: "semibold" },
  });

  // To save the PDF with a specific filename. In this case, with the invoice id
  doc.save(`Invoice_${invoice.id}.pdf`);
};

Enter fullscreen mode Exit fullscreen mode

You're free to customize the PDF any way you want to. Here's a list of jsPDF classes.

Add an onClick event to the download button and you're set.

<button onClick={() => handleDownloadPDF(invoice)}>
    Download invoice
</button>
Enter fullscreen mode Exit fullscreen mode

That's it! We've been able to build a functional invoicing app using Strapi as the backend to store the invoice data.

Demo Time!

  • Create invoice demo.
    Image description

  • Edit invoice demo.
    Image description

  • Delete invoice demo.
    Image description

If you followed the steps in this tutorial, you should have a functional invoicing app where users can create, edit, and delete invoices on the frontend. You'll also be able to manage the data on the Strapi backend and also download the invoice in a PDF format.

Conclusion

In this tutorial, we explored the steps involved in creating an invoicing app using technologies like Next.js for the frontend development, Strapi for the backend content management, and jsPDF for PDF generation.

We also learnt how to set up the development environment, creating the data collection in Strapi, how to connect the Strapi backend to the frontend, how to implement CRUD operations in Strapi, and how to integrate PDF generation functionality.

Using an invoicing app offers ready-made templates that allow quick generation of invoices and helps one keep track of outstanding invoices and due dates.

For reference, here's the GitHub repository where you can view the complete code for this project.

Additional/Related Resources

Top comments (8)

Collapse
 
kervyntjw profile image
Kervyn

Wow, love the content, it was very insightful man! How have you been finding the jspdf library? I have used it in one of my previous projects but felt that it wasn't quite well-known/well-covered by others online

Collapse
 
jully profile image
Juliet Ofoegbu

Yeah, that's true. jspdf isn't a very popular library. I came across it when I was doing a research on pdf libraries I can integrate into the project.

I've used it in one or two personal projects and it's been ok but I feel there should be room to allow for more advanced customization

Collapse
 
kervyntjw profile image
Kervyn

Ahh, we came across it the same way haha!

What advanced customizations do you wish the library had?

Thread Thread
 
jully profile image
Juliet Ofoegbu

More like advanced customization for the layout in general. I know there are options of listing out items and using tables, but some other layout customization option would be great

Collapse
 
fadhilsaheer profile image
Fadhil ⚡

Great content man, appreciate the effort 👍

Collapse
 
anmolbaranwal profile image
Anmol Baranwal

A great project.
To be frank, I've never used Strapi, so this is definitely new to me.
Anyway, it's a long read, so I will give it a full read later.

Collapse
 
jully profile image
Juliet Ofoegbu

Thanks for this.
Yeah, you should definitely give Strapi a try. I hope you find it insightful

Collapse
 
miketeddyomondi profile image
Mike Teddy Omondi

Great content Oma!!!...
May you grant me the go-ahead to dockerize this application for cloud native needs…and then make the pull request of the change…