3

Project Details:

I have a full stack project using ReactJS for the View, NodeJS for back-end and MySQL for database. On my webserver, I'm using NGINX for the web server, and PM2 as a reverse-proxy to run my node instance.

The Objective:

What I'm trying to do is basically have a user select a StartDate and EndDate from the inputs. This will pull data from the MYSQL database and then download a PDF in the browser to the user which will display it in separate PDF's. So for example, if the record contained 30 results, it would be one PDF with 30 separate pages.

The Problem:

I got all this to work fine on my localhost where it downloads the PDF successfully in the browser to the user but when I push it to a staging or production server it doesn't download the PDF in the browser at all and I'm not sure why.

On staging and live throw the same error in the console.log:

enter image description here

On staging here is a screenshot of the network request. In the response there is no response at all that I get back it's just blank but on localhost I do get a response:

However, on localhost the the PDF gets generated successfully and is downloaded in the browser no problem when the user clicks "Generate PDF" button. Here is a screenshot of the network request and the response it gets back:

I have a ton of code so I'll try to show only the relevant problem:

View (ReactJS):

import React, { useEffect, useState } from "react";

import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";

import Moment from "moment";

import AppController from "../../controllers/appController";
import Detailedservice from "../../services/Detailedservice";

const DetailedReport = () => {
  const [dateValues, handleDateChange] = useState({
    startDate: "",
    endDate: "",
  });
  const [emptyText, setEmpty] = useState(false);
  const [sucessText, setSuccess] = useState(false);
  const [loadingState, isLoading] = useState(false);
  const [scheduleData, setSchedule] = useState({
    startDate: "",
    endDate: "",
    bathData: [],
  });

  const handleChange = (e) => {
    handleDateChange({ ...dateValues, [e.target.name]: e.target.value });
  };

    const data = {
      selectedDates: {
        startDate: dateValues.startDate,
        endDate: dateValues.endDate,
      },
      requestedData: scheduleData,
    };

     // This is what generates the PDF
    const viewPdf = await Detailedservice.pdfTemplate(data);

    // These lines download the PDF in the browser for the user

    var link = document.createElement("a");
    // Location of where the download file is located in
    link.href = AppController.downloadPDF() + "MM_Report.pdf";
    link.target = "_blank";
    // Name of download file
    link.download = "MM_Report.pdf";
    link.dispatchEvent(new MouseEvent("click"));
  };

  return (
    <div className="detailedreportContainer">
      <div className="container">
          <div className="row d-flex justify-content-center">
            <div className="col-lg-2">
              <div className="daterange" style={{ marginTop: "150px" }}>
                <div className="daterangeCenter">
                  <TextField
                    id="startDate"
                    label="Start Date"
                    name="startDate"
                    type="date"
                    InputLabelProps={{
                      shrink: true,
                    }}
                    onChange={handleChange}
                  />
                </div>
              </div>
            </div>
            <div className="col-lg-2">
              <div className="daterange" style={{ marginTop: "150px" }}>
                <div className="daterangeCenter d-flex justify-content-center">
                  <TextField
                    id="endDate"
                    label="End Date"
                    name="endDate"
                    type="date"
                    InputLabelProps={{
                      shrink: true,
                    }}
                    onChange={handleChange}
                  />
                </div>
              </div>
            </div>
          </div>
          <div
            className="row d-flex justify-content-center"
            style={{ marginTop: "20px" }}
          >
            <Button
              variant="contained"
              color="primary"
              disabled={
                !dateValues.startDate || !dateValues.endDate || emptyText
                  ? true
                  : false
              }
              onClick={fetchSchedule}
            >
              {loadingState ? (
                <i
                  className={"fas fa-circle-notch fa-spin"}
                  style={{ fontSize: "24px" }}
                />
              ) : (
                "Generate Report"
              )}
            </Button>
          </div>
        </div>
      </div>
    </div>
  );
};

export default DetailedReport;

Model: (This is what creates the PDF what it looks like)

var db = require("../dbconnection");
var htmltopdf = require("html-pdf");
const path = require("path");

var options = {
  width: "8.5in",
  height: "11in",
  format: "Letter",
};

var detailedReport = {
  getSchedule: function (data, callback) {
    console.log(data);
    db.query(
      "SELECT Subject, StartTime, EndTime, FullName, Resident, CommunicationMethod, Email, Phone, Reason from schedule where DateCreated between ? AND ? order by StartTime, EndTime asc",
      [data.startDate, data.endDate],
      callback
    );
  },

  pdfMessage: function (data) {
    let scheduleData = data.requestedData;
    let dateonDocument = data.selectedDates;
    var message = "";

    // Looping over all schedules chosen and displaying them in multiple batch pdfs. This is possible using the following class "<div style='page-break-before: always'></div>" which should be used to display data on different pages

    message += "<html>";
    message += "<head>";
    message += "<meta charset='utf-8' />";
    message += "<style>";
    message += "* {";
    message += "border: 0;";
    message += "margin: 0;";
    message += "padding: 0;";
    message += "}";
    message += "body {";
    message += "font-family: cursive, Helvetica, sans-serif;";
    message += "width: 100%;";
    message += "margin: 30px;";
    message += "color: #000;";
    message += "}";
    message += "#uniqueTable,";
    message += "#uniqueTable td,";
    message += "#uniqueTable th {";
    message += "border: 1px solid #000;";
    message += "}";
    message += "table {";
    message += "border-collapse: collapse;";
    message += "width: 90%;";
    message += "margin: 10px 0;";
    message += "text-align: left;";
    message += "}";
    message += "td {";
    message += "font-size: 20px;";
    message += "padding: 10px;";
    message += "}";
    message += "p {";
    message += "padding: 3px 0;";
    message += "font-size: 14px;";
    message += "}";
    message += "img {";
    message += "padding-left: 60px;";
    message += "padding-bottom: 10px;";
    message += "display: block;";
    message += "}";
    message += "h1 {";
    message += "font-size: 18px;";
    message += "padding: 10px 0;";
    message += "color: #35a768;";
    message += "}";
    message += "h2 {";
    message += "font-size: 18px;";
    message += "font-weight: bold;";
    message += "color: #000;";
    message += "}";
    message += "hr {";
    message += "border: 1px solid #000;";
    message += "width: 90%;";
    message += "}";
    message += "table th {";
    message += "font-size: 16px;";
    message += "color: #0094e9;";
    message += "font-weight: bold;";
    message += "}";
    message += "</style>";
    message += "</head>";
    message += "<body>";

    message += "<div style='text-align:center'>";
    message += "<h1>Report</h1>";
    message +=
      "<p>" +
      dateonDocument.startDate +
      " - " +
      dateonDocument.endDate +
      "</p>";
    message += "</div>";
    message += "<div style='page-break-before: always'></div>";

    scheduleData.forEach((data) => {
      message += "<div id='pageContent'>";
      message += "<table>";
      message += "<tr>";
      message += "<td>";
      message += "<img";
      message +=
        "src='https://amazonaws.com/mmlogomain.jpg'";
      message += "style='width: 200px; margin: auto'";
      message += "/>";
      message += "</td>";
      message += "</tr>";
      message += "</table>";

      message +=
        "<div id='header' style='background-color: #e0f2ff; padding: 20px'>";
      message += "<h1 style='color: #000; font-size: 24px'>";
      message += "Detailed Report";
      message += "</h1>";
      message += "</div>";
      message +=
        "<div id='subheader' style='background-color: #0094e9; height: 30px'></div>";
      message += "<table id='uniqueTable'>";
      message += "<tbody>";
      message += "<tr>";
      message += "<td>";
      message += "<h1>Date/Time Scheduled:</h1>";

      message += "<p>" + data.StartTime + " - " + data.EndTime + "</p>";

      message += "</td>";
      message += "</tr>";
      message += "<tr>";
      message += "<td>";
      message += "<h1>Communication Method:</h1>";
      message += "<p>" + data.CommunicationMethod + "</p>";
      message += "</td>";
      message += "</tr>";
      message += "</tbody>";
      message += "</table>";
      message += "<table id='uniqueTable'>";
      message += "<tbody>";
      message += "<tr>";
      message += "<td>";
      message += "<h1>Visitor's Full Name:</h1>";
      message += "<p>" + data.FullName + "</p>";
      message += "</td>";
      message += "<td>";
      message += "<h1>Resident Requesting to Speak to:</h1>";
      message += "<p>" + data.Resident + "</p>";
      message += "</td>";
      message += "</tr>";
      message += "<tr>";
      message += "<td>";
      message += "<h1>Phone Number:</h1>";
      message += "<p>" + data.Phone + "</p>";
      message += "</td>";
      message += "<td rowspan='2'>";
      message += "<h1>Reason for Visit:</h1>";
      message += "<p>" + data.Reason + "</p>";
      message += "</td>";
      message += "</tr>";
      message += "<tr>";
      message += "<td>";
      message += "<h1>Email Address:</h1>";
      message += "<p>" + data.Email + "</p>";
      message += "</td>";
      message += "</tr>";
      message += "</tbody>";
      message += "</table>";
      message += "</div>";
      message += "<div style='page-break-before: always'></div>";
    });

    message += "</body>";
    message += "</html>";

    return message;

  },

  pdfTemplate: function (data, callback) {

    let pdfPath = "MM_Report.pdf";

    htmltopdf
      .create(detailedReport.pdfMessage(data), options)
      .toFile(
        path.join(__dirname, "../public/reports/" + pdfPath),
        function (err, res) {
          callback(res);
        }
      );
  },
};

module.exports = detailedReport;

Routes

var express = require("express");
var router = express.Router();
var detailedReport = require("../models/detailedReport");

router.post("/getSchedule", function (req, res) {
  detailedReport.getSchedule(req.body, function (err, rows) {
    if (err) {
      res.json(err);
      console.log(err);
    } else {
      res.send(rows);
    }
  });
});

router.post("/pdfTemplate", function (req, res) {
  detailedReport.pdfTemplate(req.body, function (err, rows) {
    if (err) {
      res.json(err);
    } else {
      res.json(rows);
    }
  });
});

module.exports = router;

Service:

import serviceBase from "./serviceBase";

const scheduleService = {
  getSchedule: (data) => serviceBase.post("/api/getSchedule", data),
  pdfTemplate: (data) => serviceBase.post("/api/pdfTemplate", data),
};

export default scheduleService;

enter image description here

enter image description here

enter image description here

InoCuro
  • 135
  • 12
  • You are returning a path which is file name on the server, why should that work in the server mode? You should using a endpoint to expose the data of the file and not the file path – Tarun Lalwani Mar 22 '21 at 07:06
  • I'm not sure I understand the file path that is in the screenshot is the one from localhost. However, the one on staging server there is no response in the network request. – InoCuro Mar 25 '21 at 06:02

1 Answers1

0

As I understand correctly /pdfTemplate?t=xyz returns the pdf's path.

You need to make sure that the pdf is on the server (../web/uploads/file.pdf) and not on your local directory (C://docs/file.pdf)

Another solution

  • instead of returning the path, return an encoded base64 string of your pdf;
  • on your View decode the result
  • transform it into a byte array and create a Blob object from it
  • create a link from this object and open it

Example:

//content - encoded base64 string
generateFile(content) {
    const byteCharacters = atob(content);
    const byteArrays = [];

    for (let offset = 0; offset < byteCharacters.length; offset += 512) {
        const slice = byteCharacters.slice(offset, offset + 512);

        const byteNumbers = new Array(slice.length);
        for (let i = 0; i < slice.length; i++) {
            byteNumbers[i] = slice.charCodeAt(i);
        }

        const byteArray = new Uint8Array(byteNumbers);
        byteArrays.push(byteArray);
    }

    const blob = new Blob(byteArrays, {type: "application/pdf"});

    const link = document.createElement("a");
    link.href = window.URL.createObjectURL(blob);
    link.target = '_black';
    link.click();
}
Getsumi3
  • 211
  • 2
  • 10
  • it turns out I did a try and catch within my block where it spawns the pdf on the server and it returned this. I attached screenshot – InoCuro Mar 27 '21 at 00:38