DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding a Printable Monthly Report & SLA Indicators to Albaida‑Tickets (Next.js 13)

Adding a Printable Monthly Report & SLA Indicators to Albaida‑Tickets (Next.js 13)

TL;DR: I added a client‑side “Print / Save PDF” button and SLA‑based priority alerts to the admin panel. The change required a tiny UI component, a few CSS tweaks, and extending the priority catalog with max‑response times, all while keeping the app fully server‑rendered.


The Problem

The committee needed a monthly report they could hand‑out in meetings. The existing /panel/reporte page rendered a table of tickets, but there was no way to export it as a PDF or a printer‑friendly view.

A second, related pain point was the lack of SLA visibility: tickets have priorities (urgente, alta, media, baja) with defined maximum resolution days, but the UI never highlighted tickets that were overdue, nor did the daily summary emit alerts.

Symptoms:

  • Clicking “Print” in the browser printed the whole layout, including navigation and footers – not a clean report.
  • No visual cue when a ticket exceeded its allowed response time, causing the support team to miss deadlines.

What I Tried First

Server‑side PDF with Puppeteer

I first attempted to generate a PDF on the server:

// lib/pdf.ts (initial attempt)
import puppeteer from "puppeteer";

export async function generateReportHTML() {
  const html = await renderToString(<ReportPage />);
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(html);
  const pdf = await page.pdf({ format: "A4" });
  await browser.close();
  return pdf;
}
Enter fullscreen mode Exit fullscreen mode

Why it failed:

  • The app runs on Vercel’s serverless functions; launching a headless Chromium instance exceeds the memory/time limits.
  • The PDF generation added latency to the request and blocked other admin actions.

CSS @media print Hacks

Next I tried to hide navigation with a global @media print rule, but the rule leaked into other pages, breaking the public site’s print styles.

Result: Inconsistent output and a maintenance nightmare.


The Implementation

1. Client‑side Print Button (no server cost)

I moved the print trigger to a client component (app/panel/reporte/PrintButton.tsx). This isolates the window.print() call to the browser, avoiding any server overhead.

// app/panel/reporte/PrintButton.tsx
"use client";

export function PrintButton() {
  return (
    <button
      type="button"
      className="btn"
      onClick={() => window.print()}
    >
      Imprimir / Guardar PDF
    </button>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • use client tells Next.js that this file should be bundled for the browser.
  • The button lives next to the report table, so the UI stays cohesive.

2. Printable Layout with Scoped CSS

I added a small CSS block in app/globals.css that only applies when printing the report page:

/* app/globals.css */
@media print {
  /* Hide the main navigation and footer */
  .no-print, .footer, nav { display: none !important; }

  /* Expand the table to full width */
  .report-table { width: 100%; }

  /* Page break after each KPI section */
  .kpi { page-break-inside: avoid; }
}
Enter fullscreen mode Exit fullscreen mode

Only the report page imports the no-print class on its wrapper, so other pages remain unaffected.

3. Wiring the Button into the Report Page

The new component is imported into app/panel/reporte/page.tsx. I only needed a few lines; the rest of the file (≈200 LOC) already builds the ticket list.

// app/panel/reporte/page.tsx (excerpt)
import Link from "next/link";
import { sql, type Ticket } from "@/lib/db";
import { requireStaff } from "@/lib/session";
import { PrintButton } from "./PrintButton";

export default async function ReportPage() {
  await requireStaff();

  const tickets = await sql<Ticket>`
    SELECT * FROM tickets
    WHERE created_at >= (CURRENT_DATE - INTERVAL '30 days')
    ORDER BY created_at DESC
  `;

  return (
    <section className="report-page no-print">
      <h1>Reporte Mensual</h1>
      <PrintButton />
      <TicketTable tickets={tickets} />
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode
  • The wrapper section.report-page.no-print gets hidden when printing, leaving only the table and header.

4. Adding SLA Data to the Priority Catalog

I extended lib/catalog.ts with a new PRIORITY_SLA_DAYS map:

// lib/catalog.ts
export const PRIORITY: Record<string, string> = {
  urgente: "Urgente",
  alta: "Alta",
  media: "Media",
  baja: "Baja",
};

/** Maximum response time per priority (in days). */
export const PRIORITY_SLA_DAYS: Record<string, number> = {
  urgente: 1,
  alta: 3,
  media: 7,
  baja: 14,
};
Enter fullscreen mode Exit fullscreen mode

Now the server can compute whether a ticket is overdue.

5. Highlighting Overdue Tickets

In app/panel/page.tsx I added a helper that flags tickets past their SLA:

// app/panel/page.tsx (excerpt)
import { PRIORITY_SLA_DAYS } from "@/lib/catalog";

function isOverdue(ticket: Ticket): boolean {
  const slaDays = PRIORITY_SLA_DAYS[ticket.priority];
  const created = new Date(ticket.created_at);
  const deadline = new Date(created);
  deadline.setDate(created.getDate() + slaDays);
  return new Date() > deadline && ticket.status !== "resuelto";
}
Enter fullscreen mode Exit fullscreen mode

The UI now renders a red badge next to overdue rows:

// Inside the table row component
<tr className={isOverdue(ticket) ? "overdue" : ""}>
  <td>{ticket.id}</td>
  <td>{PRIORITY[ticket.priority]}</td>
  <td>{ticket.subject}</td>
  {/* … */}
</tr>
Enter fullscreen mode Exit fullscreen mode

Corresponding CSS:

/* app/globals.css */
tr.overdue { background-color: #ffe5e5; }
tr.overdue td { color: #c00; }
Enter fullscreen mode Exit fullscreen mode

6. Daily Summary Alert

The daily summary page (app/panel/page.tsx) now aggregates overdue counts and displays a banner:

// After fetching tickets
const overdueCount = tickets.filter(isOverdue).length;

return (
  <>
    {overdueCount > 0 && (
      <div className="alert alert-danger">
        ⚠️ {overdueCount} tickets están fuera de SLA.
      </div>
    )}
    {/* Rest of the dashboard */}
  </>
);
Enter fullscreen mode Exit fullscreen mode

7. Minimal Impact on Build Size

Because the print button lives in a separate client component, the server bundle remains unchanged. I verified the bundle size with next build – the increase is < 5 KB gzipped.


Key Takeaway

When you need a quick “export” feature in a Next.js app, avoid server‑side PDF generation unless you have a dedicated worker. A simple client‑side window.print() combined with scoped @media print styles gives you a printable view with zero server cost, and you can keep the rest of your app fully server‑rendered.


What's Next

  • Replace the browser‑only print with a headless PDF service (e.g., Chrome‑as‑a‑service) for automated email delivery.
  • Add a progress bar that visualizes remaining SLA days per ticket.
  • Write unit tests for isOverdue covering edge cases (leap years, timezone shifts).

Roberto Luna Osorio – Full Stack Developer & Project Lead

Playa del Carmen, México

vibecoding


Part of my Build in Public series — sharing the real process of building SaaS projects from Playa del Carmen, México.

Repo: zaerohell/albaida-tickets · 2026-09-30

#playadev #buildinpublic

Top comments (0)