Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: change pdf generation to client side for better performance #1553

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,19 +33,6 @@ RUN npm prune --omit=dev
# Finally, build the production image with minimal footprint
FROM base AS release

# Install dependencies
RUN apt-get update && apt-get install -y \
chromium \
fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*

# Set environment variables
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
PUPPETEER_SKIP_DOWNLOAD=true \
CHROME_EXECUTABLE_PATH="/usr/bin/chromium"


COPY --from=build /src/node_modules /src/node_modules
COPY --from=build /src/app/database /src/app/database
COPY --from=build /src/build /src/build
Expand Down
7 changes: 0 additions & 7 deletions Dockerfile.image
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,6 @@ RUN npm prune --omit=dev
# Finally, build the production image with minimal footprint
FROM base AS release

# Install packages needed for pupeteer
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y chromium chromium-sandbox && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives

ENV CHROME_EXECUTABLE_PATH="/usr/bin/chromium"

COPY --from=build /src/node_modules /src/node_modules
COPY --from=build /src/app/database /src/app/database
COPY --from=build /src/build /src/build
Expand Down
226 changes: 191 additions & 35 deletions app/components/booking/generate-booking-pdf.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { useState } from "react";
import type { RefObject } from "react";
import { useEffect, useRef, useState } from "react";
import type { Asset, Booking } from "@prisma/client";
import { useReactToPrint } from "react-to-print";
import { Button } from "~/components/shared/button";

import { tw } from "~/utils/tw";
import type { PdfDbResult } from "~/modules/booking/pdf-helpers";
import { SERVER_URL } from "~/utils/env";
import { Dialog, DialogPortal } from "../layout/dialog";
import { DateS } from "../shared/date";
import { Spinner } from "../shared/spinner";

export const GenerateBookingPdf = ({
Expand All @@ -17,20 +21,29 @@ export const GenerateBookingPdf = ({
};
timeStamp: number;
}) => {
const [iframeLoaded, setIframeLoaded] = useState(false);
const totalAssets = booking.assets.length;
const url = `/bookings/${booking.id.toString()}/generate-pdf/booking-checklist-${new Date()
.toISOString()
.slice(0, 10)}.pdf?timeStamp=${timeStamp}`;
const handleIframeLoad = () => {
setIframeLoaded(true);
};
const componentRef = useRef<HTMLDivElement>(null);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [pdfMeta, setPdfMeta] = useState<PdfDbResult | null>(null);
const [isFetchingBookings, setIsFetchingBookings] = useState(true);

const handleMobileView = () => {
window.location.href = url;
};
useEffect(() => {
if (isDialogOpen) {
fetch(`/api/bookings/${booking.id}/generate-pdf`)
.then((response) => response.json())
.then((data) => {
setPdfMeta(data.pdfMeta);
})
.finally(() => {
setIsFetchingBookings(false);
});
}
}, [booking, isDialogOpen]);

const [isDialogOpen, setIsDialogOpen] = useState(false);
const handlePrint = useReactToPrint({
content: () => componentRef.current,
documentTitle: `booking-${booking.name}-${timeStamp}`,
});

const handleOpenDialog = () => {
setIsDialogOpen(true);
Expand Down Expand Up @@ -58,35 +71,31 @@ export const GenerateBookingPdf = ({
onClose={handleCloseDialog}
className="h-[90vh] w-full py-0 md:h-[calc(100vh-4rem)] md:w-[90%]"
title={
<div>
<div className="mx-auto w-full max-w-[210mm] border p-4 text-center">
<h3>Generate booking checklist for "{booking?.name}"</h3>
<p>You can either preview or download the PDF.</p>
{!isFetchingBookings && (
<div className="mt-4">
<Button onClick={handlePrint}>Download PDF</Button>
</div>
)}
</div>
}
>
<div className="flex h-full flex-col px-6">
<div className="grow">
{/** Show spinner if no iframe */}
{!iframeLoaded && (
<div className="m-4 flex h-full flex-1 flex-col items-center justify-center text-center">
<Spinner />
<p className="mt-2">Generating PDF...</p>
</div>
)}
{totalAssets && (
<div
className={tw(iframeLoaded ? "block" : "hidden", "h-full")}
>
<iframe
id="pdfPreview"
width="100%"
height="100%"
onLoad={handleIframeLoad}
src={url}
title="Booking PDF"
allowFullScreen={true}
/>
{isFetchingBookings ? (
<div className="flex h-full flex-col items-center justify-center gap-2">
<div>Generating PDF preview...</div>
<div>
<Spinner />
</div>
</div>
) : (
<BookingPDFPreview
pdfMeta={pdfMeta}
componentRef={componentRef}
/>
)}
</div>
<div className="flex justify-end gap-3 py-4">
Expand All @@ -105,10 +114,157 @@ export const GenerateBookingPdf = ({
width="full"
name="generate pdf"
disabled={!totalAssets}
onClick={handleMobileView}
// onClick={handleMobileView}
>
Generate overview PDF
</Button>
</>
);
};

const BookingPDFPreview = ({
componentRef,
pdfMeta,
}: {
componentRef: RefObject<HTMLDivElement>;
pdfMeta: PdfDbResult | null;
}) => {
if (!pdfMeta) return null;

const { booking, organization, assets, assetIdToQrCodeMap } = pdfMeta;
const custodianName = booking.custodianUser
? `${booking.custodianUser.firstName} ${booking.custodianUser.lastName} <${booking.custodianUser.email}>`
: booking.custodianTeamMember?.name;

return (
<div className="border bg-gray-200 py-4">
<style>
{`@media print {
@page {
margin: 10mm; /* Adjust margin size as needed */
size: A4;
}
.pdf-wrapper {
margin: 0;
padding: 0;
}
}`}
</style>
<div
className="pdf-wrapper mx-auto w-[200mm] bg-white p-[10mm] font-inter"
ref={componentRef}
>
<div className="mb-5 flex justify-between">
<div>
<h3 className="m-0 p-0 text-gray-600">{organization?.name}</h3>
<h1 className="mt-0.5 text-xl font-medium">
Booking checklist for {booking?.name}
</h1>
</div>
<div className="text-gray-500">
{booking.name} | <DateS date={new Date()} />
</div>
</div>

<section className="mb-5 mt-2.5 border border-gray-300">
<div className="flex border-b border-gray-300 p-2">
<span className="min-w-[150px] text-sm font-medium">Booking</span>
<span className="grow text-gray-600">{booking?.name}</span>
</div>
<div className="flex border-b border-gray-300 p-2">
<span className="min-w-[150px] text-sm font-medium">Custodian</span>
<span className="grow text-gray-600">{custodianName}</span>
</div>
<div className="flex border-b border-gray-300 p-2">
<span className="min-w-[150px] text-sm font-medium">
Booking period
</span>
<span className="grow text-gray-600">
{pdfMeta?.from && pdfMeta?.to
? `${pdfMeta.from} - ${pdfMeta.to}`
: ""}
</span>
</div>
<div className="flex p-2">
<span className="min-w-[150px] text-sm font-medium">
Description
</span>
<span className="grow whitespace-pre-wrap text-gray-600">
{booking?.description}
</span>
</div>
</section>

<table className="w-full border-collapse border border-gray-300">
<thead>
<tr>
<th className="w-10 border-b border-r border-gray-300 p-2.5 text-left text-xs font-medium">
#
</th>
<th className="w-20 min-w-[76px] border-b border-r border-gray-300 p-2.5 text-left text-xs font-medium">
Image
</th>
<th className="w-[30%] border-b border-r border-gray-300 p-2.5 text-left text-xs font-medium">
Name
</th>
<th className="w-24 border-b border-r border-gray-300 p-2.5 text-left text-xs font-medium">
Kit
</th>
<th className="w-24 border-b border-r border-gray-300 p-2.5 text-left text-xs font-medium">
Category
</th>
<th className="w-24 border-b border-r border-gray-300 p-2.5 text-left text-xs font-medium">
Location
</th>
<th className="min-w-[120px] border-b border-r border-gray-300 p-2.5 text-left text-xs font-medium">
Code
</th>
</tr>
</thead>
<tbody>
{assets.map((asset, index) => (
<tr key={asset.id} className="align-top">
<td className="border-b border-r border-gray-300 p-2.5 text-sm text-gray-600">
{index + 1}
</td>
<td className="border-b border-r border-gray-300 p-2.5 text-sm text-gray-600">
<img
src={
asset?.mainImage ||
`${SERVER_URL}/static/images/asset-placeholder.jpg`
}
alt="Asset"
className="!size-14 object-cover"
/>
</td>
<td className="border-b border-r border-gray-300 p-2.5 text-sm text-gray-600">
{asset?.title}
</td>
<td className="border-b border-r border-gray-300 p-2.5 text-sm text-gray-600">
{asset?.kit?.name}
</td>
<td className="border-b border-r border-gray-300 p-2.5 text-sm text-gray-600">
{asset?.category?.name}
</td>
<td className="border-b border-r border-gray-300 p-2.5 text-sm text-gray-600">
{asset?.location?.name}
</td>
<td className="border-b border-r border-gray-300 p-2.5 text-sm text-gray-600">
<div className="flex items-center gap-3">
<img
src={assetIdToQrCodeMap[asset.id] || ""}
alt="QR Code"
className="size-14 object-cover"
/>
<input type="checkbox" className="block size-5 border" />
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
Loading
Loading