Files
cotizador/src/app/api/export/excel/[id]/route.ts
T
urieljarethandClaude Fable 5 69c74faabb Restaurar funcionalidades desde stash + setup de despliegue Coolify
- Recupera el trabajo revertido (doble propuesta, retainer, niveles, API Python) desde el stash de GitHub Desktop
- Dockerfile multi-stage para Next.js (migraciones automáticas al arrancar, seed opcional via RUN_SEED)
- docker-compose.coolify.yml: postgres + web + api con healthchecks y SERVICE_FQDN_*
- Endpoint público /api/health (verifica BD) para healthchecks
- seed.ts parametrizado: conexión DB_* y credenciales SEED_* por entorno (sin passwords hardcodeadas)
- .env.example, .dockerignore, uvicorn con --proxy-headers
- Limpieza: .xlsx y __pycache__ fuera del repo

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 02:21:18 -06:00

85 lines
3.0 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/db";
import { getConfigBranding, getConfigBancaria } from "@/lib/config-helpers";
import { sanitizeFilename } from "@/lib/calculators";
import { buildCotizacionExcel, type ExcelData } from "@/lib/excel-builder";
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const [cot, branding, bancaria] = await Promise.all([
prisma.cotizacion.findUnique({
where: { id },
include: {
cliente: true,
asesor: true,
servicios: { include: { servicioCatalogo: true } },
planBucefalo: true,
},
}),
getConfigBranding(),
getConfigBancaria(),
]);
if (!cot) {
return NextResponse.json({ error: "Cotizacion no encontrada" }, { status: 404 });
}
const empresa = cot.cliente.empresa || cot.cliente.nombre;
const data: ExcelData = {
razonSocial: bancaria.razon_social || "Cotizador E3",
domicilioFiscal: bancaria.domicilio_fiscal || undefined,
numero: cot.numero,
clienteNombre: cot.cliente.nombre,
clienteEmpresa: cot.cliente.empresa || "",
asesorNombre: cot.asesor.name,
fecha: cot.fecha,
vigencia: cot.vigencia,
moneda: cot.moneda,
tipoCambio: cot.tipoCambio,
proyecto: cot.proyecto,
esquemaPago: cot.esquemaPago,
servicios: cot.servicios
.filter((s) => s.seleccionado)
.map((s) => ({
nombre: s.servicioCatalogo?.nombre || s.nombre || "Servicio",
fase: s.fase,
tipoPago: s.tipoPago,
precio: s.precio,
tiempoEntrega: s.tiempoEntrega,
entregables: Array.isArray(s.entregables) ? (s.entregables as string[]) : [],
modeloCobro: s.modeloCobro,
esPersonalizado: s.esPersonalizado,
horas: s.horas,
tarifaHora: s.tarifaHora,
montoMinimo: s.montoMinimo,
horasIncluidas: s.horasIncluidas,
opcion: s.opcion,
})),
esDoble: cot.esDoble,
opcionesMetadata: cot.opcionesMetadata as never,
planBucefaloNivel: cot.planBucefalo?.nivel ?? null,
planBucefaloPrecio: cot.planBucefalo?.precio ?? null,
colorPrimario: branding.colorPrimario || "#2563eb",
colorSecundario: branding.colorSecundario || "#1e293b",
};
const buffer = await buildCotizacionExcel(data);
return new NextResponse(new Uint8Array(buffer), {
headers: {
"Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"Content-Disposition": `attachment; filename="${sanitizeFilename(empresa)} - ${sanitizeFilename(cot.cliente.nombre)} - ${cot.numero}.xlsx"`,
},
});
} catch (error: unknown) {
console.error("Error exporting Excel:", error);
const message = error instanceof Error ? error.message : "Error interno";
return NextResponse.json({ error: message }, { status: 500 });
}
}