PDFMake en Acción: Cómo Diseñar Tickets en ReactJS con PDFMake
Tags: webdev, javascript, react, pdf
Published: 2023-10-23 15:40:12


![pdfmake](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m225iclgi11lw28oea6t.png)

Bienvenido, en este artículo aprenderás a diseñar tickets con ReactJS y PDFMake. Exploraremos juntos cómo dar vida a tus diseños de tickets para impresoras térmicas en una aplicación React, utilizando la potencia de PDFMake.

Desde la configuración básica hasta los detalles más intrincados de diseño, te guiaré paso a paso a través de este emocionante viaje de codificación. Así que, ponte cómodo y prepárate para transformar tu habilidad en la creación de comprobantes electrónicos. ¡Vamos a sumergirnos en el código y diseñar impresiones que realmente destaquen!

## Instalar PDFMake
`npm install pdfmake`

## Estructura de Carpetas

![Estructura carpetas](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h1z3t89rwyauabmskw62.png)

> Importante PDFMake define sus medidas en Point Puedes usar esta página para convertir la unidad de medida que usas a Point. [UnitConvertrs.net](https://www.unitconverters.net/length/millimeter-to-point.htm)

Todo inicia en el archivo, `createPdf.js` este será la función en el que definimos los parámetros base para PdfMake de manera que podamos reutilizar este para generar PDF de distintos tamaños y con información dinámica.

## CreatePdf.js

1. Importamos pdfMake y pdfFonts
2. Asignamos las fuentes importadas
3. Creamos nuestra función base inicializado con los parámetros necesario.
4. Definimos el documento
5. Con el parámetro output identificamos si generamos un base64 o enviamos directo a impresión.
6. Retornamos la respuesta indicando si se ejecutó correctamente el proceso o hubo algún error.

```javascript
import pdfMake from 'pdfmake/build/pdfmake';
import pdfFonts from 'pdfmake/build/vfs_fonts';

import printjs from 'print-js';

pdfMake.vfs = pdfFonts.pdfMake.vfs;

const createPdf = async (props, output = 'print') => {
  return new Promise((resolve, reject) => {
    try {
      const {
        pageSize = {
          width: 226.77,
          height: 841.88,
        },
        pageMargins = [5.66, 5.66, 5.66, 5.66],
        info = {
          title: 'F001-000001',
          author: 'maclode',
          subject: 'ticket',
          keywords: 'tck, sale',
        },
        styles = {
          header: {
            fontSize: 9,
            bold: true,
            alignment: 'center',
          },
          tHeaderLabel: {
            fontSize: 8,
            alignment: 'right',
          },
          tHeaderValue: {
            fontSize: 8,
            bold: true,
          },
          tProductsHeader: {
            fontSize: 8.5,
            bold: true,
          },
          tProductsBody: {
            fontSize: 8,
          },
          tTotals: {
            fontSize: 9,
            bold: true,
            alignment: 'right',
          },
          tClientLabel: {
            fontSize: 8,
            alignment: 'right',
          },
          tClientValue: {
            fontSize: 8,
            bold: true,
          },
          text: {
            fontSize: 8,
            alignment: 'center',
          },
          link: {
            fontSize: 8,
            bold: true,
            margin: [0, 0, 0, 4],
            alignment: 'center',
          },
        },
        content,
      } = props;

      const docDefinition = {
        pageSize, //TAMAÑO HOJA
        pageMargins, //MARGENES HOJA
        info, //METADATA PDF
        content, // CONTENIDO PDF
        styles, //ESTILOS PDF
      };

      if (output === 'b64') {
        //SI INDICAMOS QUE LA SALIDA SERA [b64] Base64
        const pdfMakeCreatePdf = pdfMake.createPdf(docDefinition);
        pdfMakeCreatePdf.getBase64((data) => {
          resolve({
            success: true,
            content: data,
            message: 'Archivo generado correctamente.',
          });
        });
        return;
      }

      //ENVIAR A IMPRESIÓN DIRECTA
      if (output === 'print') {
        const pdfMakeCreatePdf = pdfMake.createPdf(docDefinition);
        pdfMakeCreatePdf.getBase64((data) => {
          printjs({
            printable: data,
            type: 'pdf',
            base64: true,
          });
          resolve({
            success: true,
            content: null,
            message: 'Documento enviado a impresión.',
          });
        });
        return;
      }

      reject({
        success: false,
        content: null,
        message: 'Debes enviar tipo salida.',
      });
    } catch (error) {
      reject({
        success: false,
        content: null,
        message: error?.message ?? 'No se pudo generar proceso.',
      });
    }
  });
};

export default createPdf;
```

## Ticket.js
Aquí definimos el contenido del PDF y llamamos a la función `createPdf` para generar o mandar a imprimir el documento.

```javascript
import createPdf from './createPdf.js';

const generateTicket = (output) => {
  const content = [
    //DATOS EMPRESA
    {
      image:'data:image/png;base64,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AAASUVORK5CYII=', //Logo
      fit: [141.73, 56.692],
      alignment: 'center',
    },
    { text: 'MICROSOFT CORPORATION', style: 'header', margin: [0, 10, 0, 0] },
    { text: 'MICROSOFT', style: 'header' },
    { text: 'Seattle Hanford St 1701, WA 98144', style: 'header' },
    { text: 'EIN 11603314323', style: 'header' },

    //TIPO Y NUMERO DOCUMENTO
    { text: 'FACTURA ELECTRÓNICA', style: 'header', margin: [0, 10, 0, 2.25] },
    { text: 'F001-000001', style: 'header', margin: [0, 2.25, 0, 0] },

    //DATOS CEBECERA FACTURAR
    {
      margin: [0, 10, 0, 0],
      table: {
        widths: ['25%', '35%', '15%', '25%'],
        body: [
          [
            { text: 'FECHA:', style: 'tHeaderLabel' },
            { text: '2023-09-30', style: 'tHeaderValue' },
            { text: 'HORA:', style: 'tHeaderLabel' },
            { text: '00:45:10', style: 'tHeaderValue' },
          ],
          [
            { text: 'PEDIDO:', style: 'tHeaderLabel' },
            { text: 'V001-000001', style: 'tHeaderValue', colSpan: 3 },
            {},
            {},
          ],
          [
            { text: 'PROYECTO:', style: 'tHeaderLabel' },
            { text: 'P001-000001', style: 'tHeaderValue', colSpan: 3 },
            {},
            {},
          ],
          [
            { text: 'CAJERO:', style: 'tHeaderLabel' },
            { text: 'RUTH JOIN', style: 'tHeaderValue', colSpan: 3 },
            {},
            {},
          ],
          [
            { text: 'VENDEDOR:', style: 'tHeaderLabel' },
            { text: 'MARK SAM', style: 'tHeaderValue', colSpan: 3 },
            {},
            {},
          ],
        ],
      },
      layout: 'noBorders',
    },
    //TABLA PRODUCTOS
    {
      margin: [0, 10, 0, 0],
      table: {
        widths: ['20%', '20%', '30%', '30%'],
        headerRows: 2,
        body: [
          [
            {
              text: 'CÓDIGO - DESCRIPCIÓN',
              colSpan: 4,
              style: 'tProductsHeader',
            },
            {},
            {},
            {},
          ],
          [
            { text: 'CANT.', style: 'tProductsHeader' },
            { text: 'UM', style: 'tProductsHeader', alignment: 'center' },
            { text: 'PRECIO', style: 'tProductsHeader', alignment: 'right' },
            { text: 'TOTAL', style: 'tProductsHeader', alignment: 'right' },
          ],
          [
            {
              text: 'PLK180024 - Pelikano Mel Bellota 18mm (2150x2440)',
              style: 'tProductsBody',
              colSpan: 4,
            },
            {},
            {},
            {},
          ],
          [
            { text: '0.50', style: 'tProductsBody', alignment: 'center' },
            { text: 'UND', style: 'tProductsBody', alignment: 'center' },
            { text: '295.00', style: 'tProductsBody', alignment: 'right' },
            { text: '147.50', style: 'tProductsBody', alignment: 'right' },
          ],
          [
            {
              text: 'CANTOBELLOT01 - Canto Bellota 0.45x22mm',
              style: 'tProductsBody',
              colSpan: 4,
            },
            {},
            {},
            {},
          ],
          [
            { text: '40', style: 'tProductsBody', alignment: 'center' },
            { text: 'UND', style: 'tProductsBody', alignment: 'center' },
            { text: '0.90', style: 'tProductsBody', alignment: 'right' },
            { text: '36.00', style: 'tProductsBody', alignment: 'right' },
          ],
          [
            {
              text: 'CANTOBELLOT01 - Canto Bellota 0.45x22mm',
              style: 'tProductsBody',
              colSpan: 4,
            },
            {},
            {},
            {},
          ],
          [
            { text: '40', style: 'tProductsBody', alignment: 'center' },
            { text: 'UND', style: 'tProductsBody', alignment: 'center' },
            { text: '0.90', style: 'tProductsBody', alignment: 'right' },
            { text: '36.00', style: 'tProductsBody', alignment: 'right' },
          ],
          [
            {
              text: 'CANTOBELLOT01 - Canto Bellota 0.45x22mm',
              style: 'tProductsBody',
              colSpan: 4,
            },
            {},
            {},
            {},
          ],
          [
            { text: '40', style: 'tProductsBody', alignment: 'center' },
            { text: 'UND', style: 'tProductsBody', alignment: 'center' },
            { text: '0.90', style: 'tProductsBody', alignment: 'right' },
            { text: '36.00', style: 'tProductsBody', alignment: 'right' },
          ],
          [
            {
              text: 'CANTOBELLOT01 - Canto Bellota 0.45x22mm',
              style: 'tProductsBody',
              colSpan: 4,
            },
            {},
            {},
            {},
          ],
          [
            { text: '40', style: 'tProductsBody', alignment: 'center' },
            { text: 'UND', style: 'tProductsBody', alignment: 'center' },
            { text: '0.90', style: 'tProductsBody', alignment: 'right' },
            { text: '36.00', style: 'tProductsBody', alignment: 'right' },
          ],
        ],
      },
      layout: {
        hLineWidth: function (i, node) {
          return i === 2 ? 0.5 : 0;
        },
        vLineWidth: function (i, node) {
          return 0;
        },
        hLineColor: function () {
          return '#f2f0f0';
        },
        paddingTop: function (i, node) {
          return i % 2 === 0 ? 10 : 1;
        },
      },
    },
    {
      margin: [0, 10, 0, 0],
      table: {
        widths: ['25%', '35%', '15%', '25%'],
        body: [
          //TOTALES
          [
            { text: 'SUBTOTAL: S/', style: 'tTotals', colSpan: 2 },
            {},
            { text: '538.14', style: 'tTotals', colSpan: 2 },
            {},
          ],
          [
            { text: 'I.G.V: S/', style: 'tTotals', colSpan: 2 },
            {},
            { text: '96.86', style: 'tTotals', colSpan: 2 },
            {},
          ],
          [
            { text: 'TOTAL: S/', style: 'tTotals', colSpan: 2 },
            {},
            { text: '635.00', style: 'tTotals', colSpan: 2 },
            {},
          ],
          //TOTAL IMPORTE EN LETRAS
          [
            {
              text: 'IMPORTE EN LETRAS:',
              style: 'tTotals',
              alignment: 'left',
              colSpan: 4,
              margin: [0, 4, 0, 0],
            },
            {},
            {},
            {},
          ],
          [
            {
              text: 'SON: SEISCIENTOS TREINTA MIL QUINIENTOS CINCO Y CINCO CON 00/100 SOLES',
              style: 'tProductsBody',
              colSpan: 4,
            },
            {},
            {},
            {},
          ],
          //FORMAS PAGO
          [
            {
              text: 'FORMA DE PAGO:',
              style: 'tTotals',
              alignment: 'left',
              colSpan: 4,
              margin: [0, 4, 0, 0],
            },
            {},
            {},
            {},
          ],
          [{ text: 'CONTADO', style: 'tProductsBody', colSpan: 4 }, {}, {}, {}],
          [
            { text: 'EFECTIVO: S/', style: 'tTotals', colSpan: 2 },
            {},
            { text: '635.00', style: 'tTotals', colSpan: 2 },
            {},
          ],
          [
            { text: 'VISA: S/', style: 'tTotals', colSpan: 2 },
            {},
            { text: '635.00', style: 'tTotals', colSpan: 2 },
            {},
          ],
          //DATOS CLIENTE
          [
            {
              text: 'CLIENTE: ',
              style: 'tTotals',
              alignment: 'left',
              colSpan: 4,
              margin: [0, 6, 0, 0],
            },
            {},
            {},
            {},
          ],
          [
            { text: 'NOMBRES: ', style: 'tClientLabel' },
            { text: 'MADERAS CASTOREO S.A.', style: 'tClientValue', colSpan: 3 },
            {},
            {},
          ],
          [
            { text: 'DOC.ID: ', style: 'tClientLabel' },
            { text: '11155998822', style: 'tClientValue', colSpan: 3 },
            {},
            {},
          ],
          [
            { text: 'DIRECC.: ', style: 'tClientLabel' },
            {
              text: '15Z INT. 7X6 URB. JARDIN - SAN ISIDRO - LIMA',
              style: 'tClientValue',
              colSpan: 3,
            },
            {},
            {},
          ],
        ],
      },
      layout: 'noBorders',
    },
    //NOTA DE PIE
    {
      text: 'ESTIMADO CLIENTE, TIENE COMO PLAZO MAXIMO DE 5 DIAS HABILES EN RECOGER SU MERCADERÍA, DICHO ESTO SE LE COBRARÍA PENALIDAD DE ALMACEN POR EL MONTO DE S/20.00 POR DIA, GRACIAS.',
      style: 'text',
      alignment: 'justify',
      margin: [0, 5],
    },
    //QR FACTURA
    {
      stack: [
        {
          qr: '20603831404|03|B002|000131|724.94|4,752.30|30/09/2023|1|70477554|v2Ez4sKStje4NiqcXiuTcmTtPwgbrqgnXpWPltJKEhk=|',
          fit: 115,
          alignment: 'center',
          eccLevel: 'Q',
          margin: [0, 10, 0, 3],
        },
        {
          text: 'Representación impresa del comprobante original. Consulta tu comprobante aquí:',
          style: 'text',
        },
        {
          text: 'https://x.microsoft.pse.pe/cpe/ace72300-0dfb-42d2-9ed7-0ba6e3cee01f',
          link: 'https://x.microsoft.pse.pe/cpe/ace72300-0dfb-42d2-9ed7-0ba6e3cee01f',
          style: 'link',
        },
      ],
    },
    //QR PROYECTO
    {
      stack: [
        {
          qr: '20603831404|03|B002|000131|724.94|4,752.30|30/09/2023|1|70477554|v2Ez4sKStje4NiqcXiuTcmTtPwgbrqgnXpWPltJKEhk=|',
          fit: 115,
          alignment: 'center',
          eccLevel: 'Q',
          margin: [0, 10, 0, 3],
        },
        {
          text: 'Consulta el estado de tu proyecto, escanea el QR o ingrese al siguiente link:',
          style: 'text',
        },
        {
          text: 'https://x.microsoft/ace72300-0dfb-42d2-9ed7-0ba6e3cee01f',
          link: 'https://x.microsoft/cpe/ace72300-0dfb-42d2-9ed7-0ba6e3cee01f',
          style: 'link',
        },
      ],
    },
  ];

  const response = await createPdf({ content }, output);
  return response;
};

export default generateTicket;
```

## App.js
Finalmente en este archivo originaremos dos botones, con el primero generaremos el ticket para renderizar en el **DOM** y con el segundo enviar directo a impresión.

```react
import React, { useState } from 'react';
import './style.css';
import ticket from './utils/ticket.js';

export default function App() {
  const [base64, setBase64] = useState('');
  const [message, setMessage] = useState('');

  const onGenerateTicket = async (output) => {
    setBase64('');
    setMessage('');

    const response = await ticket(output);

    if (!response?.success) {
      alert(response?.message);
      return;
    }

    if (output === 'b64') {
      setBase64(response?.content ?? '');
    }

    setMessage(response?.message);

    setTimeout(() => {
      setMessage('');
    }, 2000);
  };

  return (
    <div className="mx-main-container">
      <div className="mx-btn-container">
        <button
          className="mx-btn-primary"
          onClick={() => onGenerateTicket('b64')}
        >
          GENERAR TICKET
        </button>
        <button
          className="mx-btn-secondary"
          onClick={() => onGenerateTicket('print')}
        >
          IMPRIMIR TICKET
        </button>
      </div>
      {message && <p className="mx-alert-info">{message}</p>}
      {base64 && (
        <iframe
          src={`data:application/pdf;base64,${base64}`}
          className="mx-iframe"
        />
      )}
    </div>
  );
}
```

## ¡Listo para Imprimir!
Has explorado cada rincón del fascinante proceso de diseño de tickets para impresoras térmicas en ReactJS con PDFMake. Desde la configuración inicial hasta los detalles más minuciosos, hemos dado vida a documentos que realmente destacan.

Recuerda, este viaje de codificación es solo el comienzo. Experimenta con los diseños, ajusta los estilos y personaliza cada detalle para que se adapte perfectamente a tus necesidades. ¡La impresión está en tus manos!

Espero que este artículo haya sido una guía útil en tu búsqueda de habilidades en la generación de documentos PDF. Si tienes alguna pregunta o deseas explorar más allá, no dudes en dejar tus comentarios.

## Recursos

1. Documentación oficial [PDFMake](https://pdfmake.github.io/docs/0.1/)
2. Convertir unidades de medida [Unit Convert](https://www.unitconverters.net/)
3. Editor de código [Stackblitz](https://stackblitz.com/)
4. Para imprimir en la misma pantalla [print-js](https://printjs.crabbly.com/)

{% embed https://stackblitz.com/edit/stackblitz-starters-jcuher?embed=1&file=src%2FApp.js %}