fix: ajustes dashboards tv e titulos
This commit is contained in:
parent
80abd92e78
commit
1b32638eb5
9 changed files with 609 additions and 232 deletions
176
src/app/api/dashboards/[id]/export/[format]/route.ts
Normal file
176
src/app/api/dashboards/[id]/export/[format]/route.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
import { NextResponse } from "next/server"
|
||||
import type { Browser } from "playwright"
|
||||
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { assertAuthenticatedSession } from "@/lib/auth-server"
|
||||
import { createConvexClient } from "@/server/convex-client"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
const WAIT_SELECTOR_DEFAULT = "[data-dashboard-ready='true']"
|
||||
const DEFAULT_VIEWPORT_WIDTH = 1600
|
||||
const DEFAULT_VIEWPORT_HEIGHT = 900
|
||||
|
||||
function slugifyFilename(raw: string, extension: "pdf" | "png") {
|
||||
const base = raw
|
||||
.normalize("NFKD")
|
||||
.replace(/[\u0300-\u036f]/g, "")
|
||||
.replace(/[^a-zA-Z0-9-_]+/g, "-")
|
||||
.replace(/-{2,}/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.toLowerCase()
|
||||
const safe = base.length > 0 ? base : "dashboard"
|
||||
return `${safe}.${extension}`
|
||||
}
|
||||
|
||||
type ExportFormat = "pdf" | "png"
|
||||
|
||||
function isSupportedFormat(value: string): value is ExportFormat {
|
||||
return value === "pdf" || value === "png"
|
||||
}
|
||||
|
||||
function buildDisposition(filename: string) {
|
||||
const encoded = encodeURIComponent(filename)
|
||||
return `attachment; filename="${filename}"; filename*=UTF-8''${encoded}`
|
||||
}
|
||||
|
||||
export async function GET(request: Request, { params }: { params: { id: string; format: string } }) {
|
||||
const formatParam = params.format?.toLowerCase()
|
||||
if (!isSupportedFormat(formatParam)) {
|
||||
return NextResponse.json({ error: "Formato não suportado" }, { status: 400 })
|
||||
}
|
||||
|
||||
const session = await assertAuthenticatedSession()
|
||||
if (!session) {
|
||||
return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||
}
|
||||
|
||||
const convex = createConvexClient()
|
||||
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
|
||||
let ensuredUser: { _id: Id<"users"> } | null = null
|
||||
try {
|
||||
ensuredUser = await convex.mutation(api.users.ensureUser, {
|
||||
tenantId,
|
||||
name: session.user.name ?? session.user.email,
|
||||
email: session.user.email,
|
||||
avatarUrl: session.user.avatarUrl ?? undefined,
|
||||
role: session.user.role.toUpperCase(),
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[dashboards.export] Falha ao garantir usuário no Convex", error)
|
||||
return NextResponse.json({ error: "Não foi possível preparar a exportação" }, { status: 500 })
|
||||
}
|
||||
|
||||
const viewerId = ensuredUser?._id
|
||||
if (!viewerId) {
|
||||
return NextResponse.json({ error: "Usuário não encontrado no Convex" }, { status: 403 })
|
||||
}
|
||||
|
||||
let detail: { dashboard: { name?: string | null; readySelector?: string | null } | null } | null = null
|
||||
try {
|
||||
detail = await convex.query(api.dashboards.get, {
|
||||
tenantId,
|
||||
viewerId,
|
||||
dashboardId: params.id as Id<"dashboards">,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[dashboards.export] Falha ao obter dashboard", error, { tenantId, dashboardId: params.id })
|
||||
return NextResponse.json({ error: "Não foi possível carregar o dashboard" }, { status: 500 })
|
||||
}
|
||||
|
||||
if (!detail?.dashboard) {
|
||||
return NextResponse.json({ error: "Dashboard não encontrado" }, { status: 404 })
|
||||
}
|
||||
|
||||
const requestUrl = new URL(request.url)
|
||||
const printUrl = new URL(`/dashboards/${params.id}/print`, requestUrl.origin).toString()
|
||||
const waitForSelector = detail.dashboard.readySelector ?? WAIT_SELECTOR_DEFAULT
|
||||
const width = Number(requestUrl.searchParams.get("width") ?? DEFAULT_VIEWPORT_WIDTH) || DEFAULT_VIEWPORT_WIDTH
|
||||
const height = Number(requestUrl.searchParams.get("height") ?? DEFAULT_VIEWPORT_HEIGHT) || DEFAULT_VIEWPORT_HEIGHT
|
||||
let browser: Browser | null = null
|
||||
|
||||
try {
|
||||
const { chromium } = await import("playwright")
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true })
|
||||
} catch (launchError) {
|
||||
const message = (launchError as Error)?.message ?? String(launchError)
|
||||
if (/Executable doesn't exist|Please run the following command to download new browsers/i.test(message)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Playwright browsers not installed",
|
||||
hint: "Execute: npx playwright install",
|
||||
details: message,
|
||||
},
|
||||
{ status: 501 },
|
||||
)
|
||||
}
|
||||
throw launchError
|
||||
}
|
||||
|
||||
const page = await browser.newPage({ viewport: { width, height } })
|
||||
await page.goto(printUrl, { waitUntil: "networkidle" })
|
||||
if (waitForSelector) {
|
||||
try {
|
||||
await page.waitForSelector(waitForSelector, { timeout: 15000 })
|
||||
} catch (error) {
|
||||
console.warn("[dashboards.export] Tempo excedido aguardando seletor", waitForSelector, error)
|
||||
}
|
||||
}
|
||||
|
||||
await page.emulateMedia({ media: "screen" }).catch(() => undefined)
|
||||
|
||||
if (formatParam === "pdf") {
|
||||
const scrollHeight = await page.evaluate(() =>
|
||||
Math.max(
|
||||
document.documentElement?.scrollHeight ?? 0,
|
||||
document.body?.scrollHeight ?? 0,
|
||||
window.innerHeight,
|
||||
),
|
||||
)
|
||||
const pdfBuffer = await page.pdf({
|
||||
width: `${width}px`,
|
||||
height: `${Math.max(scrollHeight, height)}px`,
|
||||
printBackground: true,
|
||||
margin: {
|
||||
top: "12px",
|
||||
bottom: "12px",
|
||||
left: "18px",
|
||||
right: "18px",
|
||||
},
|
||||
})
|
||||
const bytes = pdfBuffer instanceof Uint8Array ? pdfBuffer : new Uint8Array(pdfBuffer)
|
||||
const filename = slugifyFilename(detail.dashboard.name ?? "dashboard", "pdf")
|
||||
return new NextResponse(bytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition": buildDisposition(filename),
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const screenshot = await page.screenshot({ type: "png", fullPage: true })
|
||||
const bytes = screenshot instanceof Uint8Array ? screenshot : new Uint8Array(screenshot)
|
||||
const filename = slugifyFilename(detail.dashboard.name ?? "dashboard", "png")
|
||||
return new NextResponse(bytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "image/png",
|
||||
"Content-Disposition": buildDisposition(filename),
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("[dashboards.export] Falha ao exportar dashboard", error)
|
||||
return NextResponse.json({ error: "Falha ao exportar o dashboard" }, { status: 500 })
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close().catch(() => undefined)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import { NextResponse } from "next/server"
|
||||
|
||||
export const runtime = "nodejs"
|
||||
|
||||
type ExportRequest = {
|
||||
url?: string
|
||||
width?: number
|
||||
height?: number
|
||||
format?: "pdf" | "png"
|
||||
waitForSelector?: string
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let payload: ExportRequest
|
||||
try {
|
||||
payload = await request.json()
|
||||
} catch (error) {
|
||||
return NextResponse.json({ error: "Payload inválido" }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!payload.url) {
|
||||
return NextResponse.json({ error: "URL obrigatória" }, { status: 400 })
|
||||
}
|
||||
|
||||
const { chromium } = await import("playwright")
|
||||
const width = payload.width ?? 1920
|
||||
const height = payload.height ?? 1080
|
||||
const format = payload.format ?? "pdf"
|
||||
const waitForSelector = payload.waitForSelector ?? "[data-dashboard-ready='true']"
|
||||
|
||||
let browser: Awaited<ReturnType<typeof chromium.launch>> | null = null
|
||||
|
||||
try {
|
||||
try {
|
||||
browser = await chromium.launch({ headless: true })
|
||||
} catch (launchError) {
|
||||
const msg = (launchError as Error)?.message || String(launchError)
|
||||
// Dev-friendly response when Playwright browsers are missing
|
||||
if (/Executable doesn't exist|Please run the following command to download new browsers/i.test(msg)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Playwright browsers not installed",
|
||||
hint: "Execute: npx playwright install",
|
||||
details: msg,
|
||||
},
|
||||
{ status: 501 },
|
||||
)
|
||||
}
|
||||
throw launchError
|
||||
}
|
||||
const page = await browser.newPage({ viewport: { width, height } })
|
||||
await page.goto(payload.url, { waitUntil: "networkidle" })
|
||||
if (waitForSelector) {
|
||||
try {
|
||||
await page.waitForSelector(waitForSelector, { timeout: 15000 })
|
||||
} catch (error) {
|
||||
console.warn("waitForSelector timeout", error)
|
||||
}
|
||||
}
|
||||
|
||||
if (format === "pdf") {
|
||||
const pdf = await page.pdf({
|
||||
width: `${width}px`,
|
||||
height: `${height}px`,
|
||||
printBackground: true,
|
||||
pageRanges: "1",
|
||||
})
|
||||
const pdfBytes = new Uint8Array(pdf)
|
||||
return new NextResponse(pdfBytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/pdf",
|
||||
"Content-Disposition": "attachment; filename=dashboard.pdf",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const screenshot = await page.screenshot({ type: "png", fullPage: true })
|
||||
const screenshotBytes = new Uint8Array(screenshot)
|
||||
return new NextResponse(screenshotBytes, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "image/png",
|
||||
"Content-Disposition": "attachment; filename=dashboard.png",
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Failed to export dashboard", error)
|
||||
return NextResponse.json({ error: "Falha ao exportar o dashboard" }, { status: 500 })
|
||||
} finally {
|
||||
if (browser) {
|
||||
await browser.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue