DEV Community

Bruno
Bruno

Posted on

Full Stack setup from scratch - Node.js, Express.js, React.js and Lowdb (Part 2)

  1. Setup
  2. Filter pages to print
  3. Deploy to Heroku

The purpose of this post is to learn how to upload a pdf file and validate the file that will be sent.

Following Brazilian labor law, employees must register the punch in and the punch out of the office, and lunch breaks. A simple problem we have faced is that we have several offices in Brazil and there is a single pdf document with all employees (> 160).

To avoid wasting paper by printing all sheets or wasting time selecting only employees from 1 office, each employee needs to print their own time sheet.

The application works as follows:

  • Launch the application displaying the list of employees (first and last name), allowing to update the list by adding, editing or removing employees;
  • Select the time sheet pdf document and upload it;
  • After the upload, the employees and the corresponding page in the pdf are listed;
  • And finally just click on a button to display the list with ranges of pages to be printed;

The first step in the process was to find employee information that corresponded to the document.
In one of the company's systems I filtered the employees from Fortaleza-CE, but only the first and last names were useful.

At first I couldn't find a lib for the frontend that would read the pdf content, so I found the pdf-parse for the backend.

First, let's install the dependencies in package.json, in the root project:

npm install --save multer pdf-parse

multer is a node.js middleware for handling multipart/form-data, which is primarily used for uploading files.
pdf-parse is a pure javascript cross-platform module to extract texts from PDFs.

Then, let's add uploadRoutes.js to folder /routes and add the content:

const low = require('lowdb');
const multer = require('multer');
const fs = require('fs');
const pdf = require('pdf-parse');

const FileSync = require('lowdb/adapters/FileSync');

const adapter = new FileSync('db.json');
const db = low(adapter);

let storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, 'files')
  },
  filename: function (req, file, cb) {
    cb(null, file.originalname )
  }
})
let upload = multer({ storage }).single('file');

let usersToPrint = async (file) => {
  let dataBuffer = fs.readFileSync(file.path);

  const data = await pdf(dataBuffer);
  const pages = data.text.split('CONTROLE DE FREQÜÊNCIA');
  const namesPDF = pages.map(page => {
    let startIndex = page.lastIndexOf("Empregado:");
    if (startIndex === -1) {
      startIndex = page.lastIndexOf("Estagiário:") + 1;
    }
    return page.substring(startIndex + 17, page.lastIndexOf("CTPS: ") - 18);
  });
  namesPDF.shift();
  const toPrint = [];
  const users = db.get('users').value();
  users.map(user => {
    // user -> [BRUNO, BEZERRA]
    // name -> BRUNO BEZERRA CHAVES
    namesPDF.find((name, index) => {
      const nameList = name.split(' ');
      if (nameList.includes(user.name) && nameList.includes(user.lastName)) {
        toPrint.push({
          nameComplete: name,
          page: index + 1,
          checked: true,
          ...user
        });
      }
    });
  });
  toPrint.sort((a, b) => a.page > b.page ? 1 : -1);
  return toPrint;
}

module.exports = (app) => {

  app.post(`/api/upload`, (req, res) => {
    upload(req, res, async function (err) {
      if (err instanceof multer.MulterError) {
        return res.status(500).json(err);
      } else if (err) {
        return res.status(500).json(err);
      }

      const users = await usersToPrint(req.file);
      return res.status(200).send({ users });
    });
  })

}

The files loaded (upload) in the client, go to the /files folder. The function usersToPrint is used in /api/upload to read the pdf (full name) and filter users according users (name and last name) in database.
So, add the folder in the root project:

mkdir files

So, let's import the uploadRoutes.js file to index.js:

require('./routes/uploadRoutes')(app);

in frontend, let's install the dependencies:

npm install --save bootstrap reactstrap react-toastify

bootstrap is an open source toolkit for developing with HTML, CSS, and JS.
reactstrap is easy to use React Bootstrap 4 components.
react-toastify allow you to add notification to your app with ease.

Then, let's create a upload.service.js to upload pdf:

import axios from 'axios';

export default {
  upload: async (data, progress) => {
    let res = await axios.post(`/api/upload`, data, progress);
    return res.data || [];
  }
}

Let's create 4 files in folder front/src/components to use in application:

InputFile.js to handle pdf to upload and some validations.

import React from 'react';
import { toast } from 'react-toastify';
import { Input } from 'reactstrap';

const InputFile = (props) => {
  const maxSelectFile = (event) => {
    let files = event.target.files; // create file object
    if (files.length > 1) { 
      const msg = 'Only 1 pdf can be uploaded at a time';
      event.target.value = null; // discard selected file
      toast.error(msg);
      return false;
    }
    return true;
  }

  const checkMimeType = (event) => {
    let files = event.target.files;
    let err = '';
    const types = ['application/pdf'];
    for(let x = 0; x<files.length; x++) {
      if (types.every(type => files[x].type !== type)) {
        err += files[x].type + ' is not a supported format\n';
      }
    };

    for(var z = 0; z<err.length; z++) {
      event.target.value = null;
      toast.error(err[z]);
    }
    return true; 
  }

  const checkFileSize = (event) => {
    let files = event.target.files;
    let size = 20000000;
    let err = ''; 
    for(let x = 0; x<files.length; x++) {
      if (files[x].size > size) {
        err += files[x].type + 'is too large, please pick a smaller file\n';
      }
    }
    for(let z = 0; z<err.length; z++) {
      toast.error(err[z]);
      event.target.value = null;
    }
    return true; 
  }

  const onChangeHandler = async (event) => {
    console.log(event.target.files[0]);
    if (maxSelectFile(event) && checkMimeType(event) && checkFileSize(event)) {
      props.selectedFile(event.target.files[0]);
    }
  }

  return (
    <Input className="mt15" type="file" name="file" onChange={onChangeHandler}/>
    );
  }

  export default InputFile;

TableRow.js to show user name, user lastName, page in pdf and button to edit.

import React from 'react';
import { Button } from 'reactstrap';

const TableRow = (props) => {
  const { id, user, edit } = props;
  const name = user.name.charAt(0) + user.name.toLowerCase().slice(1);
  const lastName = user.lastName.charAt(0) + user.lastName.toLowerCase().slice(1);

  return (
    <tr key={id} className="list__item user" title={user.nameComplete}>
      <td>{user.page ? (
        <input
          type="checkbox"
          id={id}
          name={id}
          value={user.checked}
          defaultChecked={user.checked}
          onClick={() => {user.checked = !user.checked}}
          ></input>
        ) : ''}
      </td>
      <td>{name}</td>
      <td>{lastName}</td>
      <td>{user.page ? user.page  : ''}</td>
      <td>
        <Button color="info" onClick={() => edit(user)}>Edit</Button>
      </td>
    </tr>
  );
}

export default TableRow;

ModalPdf.js to show pages to print.

import React, { useState } from 'react';
import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap';

const ModalPdf = (props) => {
  const {
    onClose
  } = props;

  const [modal, setModal] = useState(true);

  const toggle = () => {
    onClose();
    setModal(!modal);
  }

  const copyToClipboard = () => {
    navigator.clipboard.writeText(props.children);
  }

  return (
    <div>
      <Modal isOpen={modal} toggle={toggle}>
        <ModalHeader toggle={toggle}>Pages</ModalHeader>
        <ModalBody>
          {props.children}
        </ModalBody>
        <ModalFooter>
          <Button onClick={copyToClipboard}>Copy To Clipboard</Button>
        </ModalFooter>
      </Modal>
    </div>
  );
}

export default ModalPdf;

ModalDuplicated.js to show users that contain the same name and last name.

import React, { useState } from 'react';
import { Modal, ModalHeader, ModalBody } from 'reactstrap';

const ModalDuplicated = (props) => {
  const {
    onClose
  } = props;

  const [modal, setModal] = useState(true);

  const toggle = () => {
    onClose();
    setModal(!modal);
  }

  return (
    <div>
      <Modal isOpen={modal} toggle={toggle}>
        <ModalHeader toggle={toggle}>Duplicates</ModalHeader>
        <ModalBody>
          {props.children}
        </ModalBody>
      </Modal>
    </div>
  );
}

export default ModalDuplicated;

Then, let's add the code in front/src/index.css:

.mt15 {
  margin-top: 15px;
}

.scroll-table {
  display: block;
  height: 400px;
  overflow-y: scroll;
}

And finally, import bootstrap and react-toastfy to front/src/index.js:

import 'bootstrap/dist/css/bootstrap.min.css';
import 'react-toastify/dist/ReactToastify.css';

Run application with command npm run dev and follow the steps:
Choose File -> Upload -> Select users to print -> Pages to print -> Copy To Clipboard

Finally, we can print only pages according employees of the office (employees saved in the database).
In this quick tutorial we saw how to upload a pdf, manipulate the content, and run validations for these situations: too many images to upload, upload an image with wrong file extension and sending an image file that is too large.

We can make many improvements and apply other good practices to the project. For example, better modularize the components, simplify the Modals, etc ... but that is for future posts.

The source code can be found in node-react-project in branch filter-pdf.

Top comments (0)