Sales reports drive sales management and business decisions. Daily, weekly, and monthly reports are standard equipment for almost every sales team, yet exporting them is rarely smooth. Backend template rendering is slow to iterate on and fights with Chinese fonts, while browser printing produces uncontrolled layouts that look different on every machine. dompdf.js moves sales report generation entirely to the frontend: sales data is fetched from an API, assembled into HTML tables in the browser, enriched with KPI summaries and chart images, and rendered to vector PDF with a single addPage call. This article walks through the complete sales report PDF workflow, including data organization, combining tables with charts, multi-page monthly reports, and common pitfalls, helping sales systems ship export capability quickly. Along the way we will cover metric definition consistency, chart integration, batch export discipline, and the validation that keeps totals honest. Whether you are adding export to a CRM, an ERP, or a lightweight sales tracker, the template patterns here adapt quickly, and because everything runs in the browser, your sales team gets instant, consistent reports without waiting on a backend queue.
Sales teams need daily reports to track the day's performance and monthly reports to analyze trends, while management wants multi-dimensional summaries by region, product, and salesperson. Typical reports include revenue, order count, average order value, collection rate, line-item lists, and year-over-year or month-over-month comparisons.
Backend export has recurring problems: report templates are coupled to business code, so any change in metric definitions means a code change and a release; large datasets make export APIs time out; and servers without Chinese fonts produce garbled reports. Sales teams end up waiting on development queues, and the reports lose their timeliness.
Browser printing is flexible but unreliable: browser settings, paper sizes, and zoom levels differ across machines, so page margins and font sizes vary unpredictably. The extra steps in the print dialog also slow down batch export, making it a poor fit for generating many reports at once.
Inconsistent metric definitions are another hidden pain. Some systems count revenue including tax, others exclude it, and the numbers never reconcile. A frontend approach concentrates the definitions in one place: a set of shared functions used by every report template, so all reports agree by construction and the definition is easy to change in one spot.
Timeliness compounds the problem. Management wants to see today's numbers, not yesterday's batch job. With frontend generation the data is pulled fresh from the API at export time, so the report reflects the current state of the system, and there is no scheduled job to miss or queue to wait behind.
Data volume is another dimension to plan for. A monthly report for a large organization can involve thousands of line items, which is more than a single page should carry. The frontend can aggregate at the source, pre-summarize by dimension, and render only the top-N rows per page, keeping the PDF fast to generate and pleasant to read.
Also think about who runs the report and how often. If managers generate reports themselves from a dashboard button, the flow should be one click with no parameters; if the operations team generates them on a schedule, provide a batch mode that reuses the same template with different date ranges.
In a frontend approach the report template is plain HTML. Adjusting a metric definition only means editing the frontend template, and the change takes effect on the next refresh with no server release. Filtering, sorting, and aggregation all happen in the browser, so report shapes stay flexible.
dompdf.js supports flex and grid layout, which makes it easy to build card-style KPI summary areas, and it supports image embedding, so charts rendered by libraries like ECharts can be converted to base64 images and embedded. The result is a professional combination of tables and charts.
Batch export is a natural strength of the frontend approach: loop through salespeople or regions, call addPage for each, and save once. Dozens of reports are generated in seconds. Combined with automatic pagination and headers and footers, multi-page monthly reports keep a uniform layout, and what you print is what you see.
Customization becomes cheap. When a sales director asks for a new dimension, such as breaking down revenue by industry or by channel, a frontend developer can add a grouping and a summary block to the template the same day. In a backend approach that request would consume a full development and release cycle, which is precisely why so many sales requests go unfulfilled.
Self-service is a bonus. Because the template is just HTML, power users can be given a sandbox to tweak colors, logos, and section order, and the exports stay consistent with the corporate style. This reduces the support burden on engineering while giving the sales team a sense of ownership over their own reporting.
A sales report has four zones: the title and reporting period at the top; a row of KPI cards in the middle laid out with flex, showing revenue, order count, average order value, and similar numbers; the line-item table below; and a bottom section with month-over-month analysis and notes.
The line-item table should include salesperson, customer, product, order amount, deal time, and order status. Right-align amounts with thousands separators, and use different colors for statuses such as closed-won, in-progress, and lost to improve readability.
For charts, render with ECharts or Chart.js, convert the canvas to a PNG base64 string, and embed it in the HTML at a sensible width. dompdf.js embeds the image proportionally, giving the report both detailed data and trend visualization for a much more professional feel.
Consider adding a commentary area for month-over-month or year-over-year changes. A short line explaining that growth came from enterprise deals turns a pile of numbers into a readable story, and management can grasp the highlights at a glance. Make the commentary a configuration item maintained by sales operations, so no engineering involvement is needed for routine updates.
Also think about the audience. A daily report for field representatives should emphasize their own numbers and next actions, while a monthly report for executives should lead with aggregates and trends. One template can support both by parameterizing the emphasis, showing different sections depending on the report type.
This example shows a monthly sales report with KPI cards, a line-item table, and an embedded chart image. It demonstrates the typical sales report structure and can be adapted directly to daily and weekly templates.
The key chart conversion code: render the chart with ECharts, call getDataURL({ type: 'png', pixelRatio: 2 }) to get a high-resolution base64 string, and dispose the chart instance afterwards to free memory. The 2x pixel ratio keeps the embedded image sharp in the PDF.
The KPI cards in the example use flex with flex:1 on each card, so widths divide evenly no matter how many metrics you show. If there are more than four or five, wrap them into two rows with flex-wrap and the layout code barely changes, which keeps the template flexible as your metric set evolves.
The full chart flow is: create an ECharts instance, render into a hidden canvas container, call getDataURL({ type: 'png', pixelRatio: 2 }) to obtain a high-resolution base64 string, and write it into an img tag in the template. Always call chart.dispose() after conversion to free memory, and do the same per chart when a report contains several, keeping memory usage flat during batch generation.
Give charts a fixed aspect ratio, for example 100 percent width and 260px height, so dompdf.js embeds them without distortion. Match the chart type to the data shape: lines for trends, bars for comparisons, pies for composition. A well-chosen chart makes the report easier to read, while a mismatched one just adds noise to a page of numbers.
import { DomPDF } from 'dompdf.js';
// Assume data has been fetched from an API
const summary = { revenue: '1,285,000', orders: 326, avgOrder: '3,941' };
const detailRows = [
{ seller: 'Zhang Wei', customer: 'Huaxin Tech', product: 'Enterprise', amount: '120,000', status: 'Closed' },
{ seller: 'Li Na', customer: 'Blue Ocean Trading', product: 'Professional', amount: '86,000', status: 'Closed' },
{ seller: 'Wang Qiang', customer: 'Yunfan Network', product: 'Basic', amount: '42,500', status: 'In progress' },
];
const detailHtml = detailRows.map(row => `
<tr>
<td style='border:1px solid #333;padding:6px'>${row.seller}</td>
<td style='border:1px solid #333;padding:6px'>${row.customer}</td>
<td style='border:1px solid #333;padding:6px'>${row.product}</td>
<td style='border:1px solid #333;padding:6px;text-align:right'>${row.amount}</td>
<td style='border:1px solid #333;padding:6px'>${row.status}</td>
</tr>
`).join('');
const reportHtml = `
<h1 style='text-align:center;font-size:16pt'>Sales Report - July 2026</h1>
<div style='display:flex;justify-content:space-between;margin:12px 0'>
<div style='flex:1;border:1px solid #333;padding:10px;text-align:center'>
<div style='font-size:10pt'>Revenue</div>
<div style='font-size:14pt;font-weight:bold'>${summary.revenue}</div>
</div>
<div style='flex:1;border:1px solid #333;padding:10px;text-align:center;margin:0 8px'>
<div style='font-size:10pt'>Orders</div>
<div style='font-size:14pt;font-weight:bold'>${summary.orders}</div>
</div>
<div style='flex:1;border:1px solid #333;padding:10px;text-align:center'>
<div style='font-size:10pt'>Avg Order Value</div>
<div style='font-size:14pt;font-weight:bold'>${summary.avgOrder}</div>
</div>
</div>
<table style='width:100%;border-collapse:collapse'>
<tr style='background:#f5f5f5'>
<th style='border:1px solid #333;padding:6px'>Seller</th>
<th style='border:1px solid #333;padding:6px'>Customer</th>
<th style='border:1px solid #333;padding:6px'>Product</th>
<th style='border:1px solid #333;padding:6px'>Amount</th>
<th style='border:1px solid #333;padding:6px'>Status</th>
</tr>
${detailHtml}
</table>
<img src='${chartBase64}' style='width:100%;margin-top:12px' />
`;
const pdf = new DomPDF();
pdf.addPage(reportHtml, { format: 'A4', margin: '20mm' });
pdf.save('sales-report-july-2026.pdf');
Compute summary metrics in JavaScript before rendering: use reduce to sum revenue, calculate average order value and month-over-month growth, and write the formatted strings into the HTML. The rendering layer only displays; it never computes, which keeps the logic clear and testable.
For multi-page exports, control content density per page: keep line items under about 30 rows per page, and split by date or product group when there are more. When calling addPage multiple times, give each page its own title so every page stands on its own.
Page numbers and headers and footers matter for long reports. dompdf.js supports headers, footers, and page numbering: put page X of Y in the footer and the report name and generation time in the header, so archived reports can be located quickly and page order is never ambiguous.
For formal monthly reports, consider adding a cover page and a table of contents. The cover carries the report title, the period, and the preparer; the contents lists the section titles and page numbers. Add the cover first, then the content pages, and save once: the document immediately feels like a proper management report rather than a loose printout.
Consistency across batches matters as much as within one report. Generate all reports for a period with the same template version and the same data snapshot, and note the generation timestamp in the header so readers know exactly how fresh the data is.
Problem one: chart images look blurry. The canvas export resolution is too low. Use pixelRatio: 2 or higher when generating the base64 image and keep the image width within the usable page width so printing stays sharp.
Problem two: flex layout shifts in the PDF. dompdf.js supports flex and grid, but prefer simple patterns like justify-content: space-between for horizontal card rows and avoid deeply nested flex structures to minimize rendering differences.
Problem three: totals disagree with line items. Aggregate metrics must be computed from the same detail data. Sum the details before rendering and compare against the API-returned totals; flag mismatches instead of rendering inconsistent numbers.
Watch the time-window logic carefully: daily, weekly, and monthly reports must have unambiguous start and end times, and cross-month generation must not double-count transactions. Refresh the data source before generating so the export reflects the latest state, and always print the generation time in the header so nobody mistakes a stale report for current numbers.
Finally, test the template with edge cases before rolling it out: a day with zero orders, a customer with a very long name, a currency with three decimals. Cheap to fix in the template, these edge cases are embarrassing and expensive if they surface in a report that has already been distributed to management.
Access control deserves a reminder in the sales context: reports contain revenue, pipeline, and quota information that is sensitive internally and externally. Gate the export button behind the same permissions that gate the underlying data, and consider watermarking PDFs with the viewer's identity when reports are distributed to large audiences.
Finally, treat the report template as a product. Collect feedback from the sales team on what is missing, add it in the template, and measure whether the report actually influences decisions. A report that nobody reads is worse than no report, and iterating on a frontend template is cheap enough to do regularly.
下面的按钮用 dompdf.js 在浏览器端实时生成 PDF,无需后端:
这是由 dompdf.js 渲染的示例 PDF 内容。