feat: reforca o branding das exportacoes

This commit is contained in:
Felipe Coutinho
2026-03-06 13:58:07 +00:00
parent 3b73c36a5c
commit 137c7b305d
3 changed files with 208 additions and 25 deletions

View File

@@ -19,6 +19,10 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { formatCurrency } from "@/lib/lancamentos/formatting-helpers"; import { formatCurrency } from "@/lib/lancamentos/formatting-helpers";
import {
getPrimaryPdfColor,
loadExportLogoDataUrl,
} from "@/lib/utils/export-branding";
import type { LancamentoItem } from "./types"; import type { LancamentoItem } from "./types";
interface LancamentosExportProps { interface LancamentosExportProps {
@@ -51,6 +55,17 @@ export function LancamentosExport({
return "-"; return "-";
}; };
const getNameWithInstallment = (lancamento: LancamentoItem) => {
const isInstallment =
lancamento.condition.trim().toLowerCase() === "parcelado";
if (!isInstallment || !lancamento.installmentCount) {
return lancamento.name;
}
return `${lancamento.name} (${lancamento.currentInstallment ?? 1}/${lancamento.installmentCount})`;
};
const exportToCSV = () => { const exportToCSV = () => {
try { try {
setIsExporting(true); setIsExporting(true);
@@ -71,7 +86,7 @@ export function LancamentosExport({
lancamentos.forEach((lancamento) => { lancamentos.forEach((lancamento) => {
const row = [ const row = [
formatDate(lancamento.purchaseDate), formatDate(lancamento.purchaseDate),
lancamento.name, getNameWithInstallment(lancamento),
lancamento.transactionType, lancamento.transactionType,
lancamento.condition, lancamento.condition,
lancamento.paymentMethod, lancamento.paymentMethod,
@@ -129,7 +144,7 @@ export function LancamentosExport({
lancamentos.forEach((lancamento) => { lancamentos.forEach((lancamento) => {
const row = [ const row = [
formatDate(lancamento.purchaseDate), formatDate(lancamento.purchaseDate),
lancamento.name, getNameWithInstallment(lancamento),
lancamento.transactionType, lancamento.transactionType,
lancamento.condition, lancamento.condition,
lancamento.paymentMethod, lancamento.paymentMethod,
@@ -145,7 +160,7 @@ export function LancamentosExport({
ws["!cols"] = [ ws["!cols"] = [
{ wch: 12 }, // Data { wch: 12 }, // Data
{ wch: 30 }, // Nome { wch: 42 }, // Nome
{ wch: 15 }, // Tipo { wch: 15 }, // Tipo
{ wch: 15 }, // Condição { wch: 15 }, // Condição
{ wch: 20 }, // Pagamento { wch: 20 }, // Pagamento
@@ -168,14 +183,33 @@ export function LancamentosExport({
} }
}; };
const exportToPDF = () => { const exportToPDF = async () => {
try { try {
setIsExporting(true); setIsExporting(true);
const doc = new jsPDF({ orientation: "landscape" }); const doc = new jsPDF({ orientation: "landscape" });
const primaryColor = getPrimaryPdfColor();
const [smallLogoDataUrl, textLogoDataUrl] = await Promise.all([
loadExportLogoDataUrl("/logo_small.png"),
loadExportLogoDataUrl("/logo_text.png"),
]);
let brandingEndX = 14;
if (smallLogoDataUrl) {
doc.addImage(smallLogoDataUrl, "PNG", brandingEndX, 7.5, 8, 8);
brandingEndX += 10;
}
if (textLogoDataUrl) {
doc.addImage(textLogoDataUrl, "PNG", brandingEndX, 8, 30, 8);
brandingEndX += 32;
}
const titleX = brandingEndX > 14 ? brandingEndX + 4 : 14;
doc.setFont("courier", "normal");
doc.setFontSize(16); doc.setFontSize(16);
doc.text("Lançamentos", 14, 15); doc.text("Lançamentos", titleX, 15);
doc.setFontSize(10); doc.setFontSize(10);
const periodParts = period.split("-"); const periodParts = period.split("-");
@@ -197,8 +231,15 @@ export function LancamentosExport({
periodParts.length === 2 periodParts.length === 2
? `${monthNames[Number.parseInt(periodParts[1], 10) - 1]}/${periodParts[0]}` ? `${monthNames[Number.parseInt(periodParts[1], 10) - 1]}/${periodParts[0]}`
: period; : period;
doc.text(`Período: ${formattedPeriod}`, 14, 22); doc.text(`Período: ${formattedPeriod}`, titleX, 22);
doc.text(`Gerado em: ${new Date().toLocaleDateString("pt-BR")}`, 14, 27); doc.text(
`Gerado em: ${new Date().toLocaleDateString("pt-BR")}`,
titleX,
27,
);
doc.setDrawColor(...primaryColor);
doc.setLineWidth(0.5);
doc.line(14, 31, doc.internal.pageSize.getWidth() - 14, 31);
const headers = [ const headers = [
[ [
@@ -216,7 +257,7 @@ export function LancamentosExport({
const body = lancamentos.map((lancamento) => [ const body = lancamentos.map((lancamento) => [
formatDate(lancamento.purchaseDate), formatDate(lancamento.purchaseDate),
lancamento.name, getNameWithInstallment(lancamento),
lancamento.transactionType, lancamento.transactionType,
lancamento.condition, lancamento.condition,
lancamento.paymentMethod, lancamento.paymentMethod,
@@ -229,26 +270,28 @@ export function LancamentosExport({
autoTable(doc, { autoTable(doc, {
head: headers, head: headers,
body: body, body: body,
startY: 32, startY: 35,
tableWidth: "auto",
styles: { styles: {
font: "courier",
fontSize: 8, fontSize: 8,
cellPadding: 2, cellPadding: 2,
}, },
headStyles: { headStyles: {
fillColor: [59, 130, 246], fillColor: primaryColor,
textColor: 255, textColor: 255,
fontStyle: "bold", fontStyle: "bold",
}, },
columnStyles: { columnStyles: {
0: { cellWidth: 20 }, // Data 0: { cellWidth: 24 }, // Data
1: { cellWidth: 40 }, // Nome 1: { cellWidth: 58 }, // Nome
2: { cellWidth: 25 }, // Tipo 2: { cellWidth: 22 }, // Tipo
3: { cellWidth: 25 }, // Condição 3: { cellWidth: 22 }, // Condição
4: { cellWidth: 30 }, // Pagamento 4: { cellWidth: 28 }, // Pagamento
5: { cellWidth: 25 }, // Valor 5: { cellWidth: 24 }, // Valor
6: { cellWidth: 30 }, // Categoria 6: { cellWidth: 30 }, // Categoria
7: { cellWidth: 30 }, // Conta/Cartão 7: { cellWidth: 30 }, // Conta/Cartão
8: { cellWidth: 30 }, // Pagador 8: { cellWidth: 31 }, // Pagador
}, },
didParseCell: (cellData) => { didParseCell: (cellData) => {
if (cellData.section === "body" && cellData.column.index === 5) { if (cellData.section === "body" && cellData.column.index === 5) {
@@ -262,7 +305,7 @@ export function LancamentosExport({
} }
} }
}, },
margin: { top: 32 }, margin: { top: 35 },
}); });
doc.save(getFileName("pdf")); doc.save(getFileName("pdf"));

View File

@@ -24,6 +24,10 @@ import {
formatPercentageChange, formatPercentageChange,
formatPeriodLabel, formatPeriodLabel,
} from "@/lib/relatorios/utils"; } from "@/lib/relatorios/utils";
import {
getPrimaryPdfColor,
loadExportLogoDataUrl,
} from "@/lib/utils/export-branding";
import type { FilterState } from "./types"; import type { FilterState } from "./types";
interface CategoryReportExportProps { interface CategoryReportExportProps {
@@ -189,26 +193,52 @@ export function CategoryReportExport({
} }
}; };
const exportToPDF = () => { const exportToPDF = async () => {
try { try {
setIsExporting(true); setIsExporting(true);
// Create PDF // Create PDF
const doc = new jsPDF({ orientation: "landscape" }); const doc = new jsPDF({ orientation: "landscape" });
const primaryColor = getPrimaryPdfColor();
const [smallLogoDataUrl, textLogoDataUrl] = await Promise.all([
loadExportLogoDataUrl("/logo_small.png"),
loadExportLogoDataUrl("/logo_text.png"),
]);
let brandingEndX = 14;
if (smallLogoDataUrl) {
doc.addImage(smallLogoDataUrl, "PNG", brandingEndX, 7.5, 8, 8);
brandingEndX += 10;
}
if (textLogoDataUrl) {
doc.addImage(textLogoDataUrl, "PNG", brandingEndX, 8, 30, 8);
brandingEndX += 32;
}
const titleX = brandingEndX > 14 ? brandingEndX + 4 : 14;
// Add header // Add header
doc.setFont("courier", "normal");
doc.setFontSize(16); doc.setFontSize(16);
doc.text("Relatório de Categorias por Período", 14, 15); doc.text("Relatório de Categorias por Período", titleX, 15);
doc.setFontSize(10); doc.setFontSize(10);
doc.text( doc.text(
`Período: ${formatPeriodLabel( `Período: ${formatPeriodLabel(
filters.startPeriod, filters.startPeriod,
)} - ${formatPeriodLabel(filters.endPeriod)}`, )} - ${formatPeriodLabel(filters.endPeriod)}`,
14, titleX,
22, 22,
); );
doc.text(`Gerado em: ${new Date().toLocaleDateString("pt-BR")}`, 14, 27); doc.text(
`Gerado em: ${new Date().toLocaleDateString("pt-BR")}`,
titleX,
27,
);
doc.setDrawColor(...primaryColor);
doc.setLineWidth(0.5);
doc.line(14, 31, doc.internal.pageSize.getWidth() - 14, 31);
// Build table data // Build table data
const headers = [ const headers = [
@@ -255,13 +285,15 @@ export function CategoryReportExport({
autoTable(doc, { autoTable(doc, {
head: headers, head: headers,
body: body, body: body,
startY: 32, startY: 35,
tableWidth: "auto",
styles: { styles: {
font: "courier",
fontSize: 8, fontSize: 8,
cellPadding: 2, cellPadding: 2,
}, },
headStyles: { headStyles: {
fillColor: [59, 130, 246], // Blue fillColor: primaryColor,
textColor: 255, textColor: 255,
fontStyle: "bold", fontStyle: "bold",
}, },
@@ -301,7 +333,7 @@ export function CategoryReportExport({
} }
} }
}, },
margin: { top: 32 }, margin: { top: 35 },
}); });
// Save PDF // Save PDF

View File

@@ -0,0 +1,108 @@
const FALLBACK_PRIMARY_COLOR: [number, number, number] = [201, 106, 58];
const RGB_PATTERN = /\d+(?:\.\d+)?/g;
function parseRgbColor(value: string): [number, number, number] | null {
if (!value.toLowerCase().startsWith("rgb")) {
return null;
}
const channels = value.match(RGB_PATTERN);
if (!channels || channels.length < 3) {
return null;
}
const red = Number.parseFloat(channels[0]);
const green = Number.parseFloat(channels[1]);
const blue = Number.parseFloat(channels[2]);
if ([red, green, blue].some((channel) => Number.isNaN(channel))) {
return null;
}
return [Math.round(red), Math.round(green), Math.round(blue)];
}
function resolveCssColor(value: string): [number, number, number] | null {
if (typeof window === "undefined" || typeof document === "undefined") {
return null;
}
const probe = document.createElement("span");
probe.style.position = "fixed";
probe.style.opacity = "0";
probe.style.pointerEvents = "none";
probe.style.color = "";
probe.style.color = value;
if (!probe.style.color) {
return null;
}
document.body.appendChild(probe);
const resolved = window.getComputedStyle(probe).color;
document.body.removeChild(probe);
return parseRgbColor(resolved);
}
export function getPrimaryPdfColor(): [number, number, number] {
if (typeof window === "undefined" || typeof document === "undefined") {
return FALLBACK_PRIMARY_COLOR;
}
const rootStyles = window.getComputedStyle(document.documentElement);
const rawPrimary = rootStyles.getPropertyValue("--primary").trim();
const rawColorPrimary = rootStyles.getPropertyValue("--color-primary").trim();
const candidates = [rawPrimary, rawColorPrimary].filter(Boolean);
for (const candidate of candidates) {
const resolved = resolveCssColor(candidate);
if (resolved) {
return resolved;
}
}
return FALLBACK_PRIMARY_COLOR;
}
export async function loadExportLogoDataUrl(
logoPath = "/logo_text.png",
): Promise<string | null> {
if (typeof window === "undefined" || typeof document === "undefined") {
return null;
}
return new Promise((resolve) => {
const image = new Image();
image.crossOrigin = "anonymous";
image.onload = () => {
const width = image.naturalWidth || image.width;
const height = image.naturalHeight || image.height;
if (!width || !height) {
resolve(null);
return;
}
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
if (!context) {
resolve(null);
return;
}
context.drawImage(image, 0, 0, width, height);
try {
resolve(canvas.toDataURL("image/png"));
} catch {
resolve(null);
}
};
image.onerror = () => resolve(null);
image.src = logoPath;
});
}