feat: reforca o branding das exportacoes
This commit is contained in:
@@ -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"));
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
108
lib/utils/export-branding.ts
Normal file
108
lib/utils/export-branding.ts
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user