Skip to content

Commit

Permalink
Merge pull request #1553 from Shelf-nu/1547-bug-generating-a-big-book…
Browse files Browse the repository at this point in the history
…ing-overview-pdf-breaks-the-whole-app

fix: change pdf generation to client side for better performance
  • Loading branch information
DonKoko authored Jan 6, 2025
2 parents d6edad3 + 8081284 commit c98aa07
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 460 deletions.
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

0 comments on commit c98aa07

Please sign in to comment.