Implementa las 4 fases de la especificación de automatización: módulo de plantillas/tarifas editable por el equipo, generación de PDF (HTML+JS vía Chrome headless) para cotizaciones/contratos/arquitecturas/cuentas de cobro, chat propio en el dashboard reutilizando el mismo motor y tools del bot de Telegram, y nuevas tools del agente para crear estos documentos end-to-end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
83 lines
2.6 KiB
Go
83 lines
2.6 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/chromedp/cdproto/page"
|
|
"github.com/chromedp/chromedp"
|
|
)
|
|
|
|
// RenderHTMLToPDF convierte un documento HTML (ya renderizado, con JS/CSS inline si aplica)
|
|
// a PDF usando Chrome headless. Es el motor de "impresión" que comparten cotizaciones,
|
|
// contratos, actas de proyecto y cuentas de cobro: la plantilla se renderiza a HTML con
|
|
// text/template y este servicio la convierte al PDF final.
|
|
//
|
|
// Requiere Chrome/Chromium instalado en el host (ver Dockerfile). La ruta del binario se
|
|
// puede forzar con la variable de entorno CHROME_EXEC_PATH; si no está definida, chromedp
|
|
// intenta ubicar un Chrome instalado en el sistema.
|
|
func RenderHTMLToPDF(html string) ([]byte, error) {
|
|
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
|
chromedp.Flag("headless", true),
|
|
chromedp.Flag("no-sandbox", true),
|
|
chromedp.Flag("disable-gpu", true),
|
|
chromedp.Flag("disable-dev-shm-usage", true),
|
|
)
|
|
if execPath := os.Getenv("CHROME_EXEC_PATH"); execPath != "" {
|
|
opts = append(opts, chromedp.ExecPath(execPath))
|
|
}
|
|
|
|
allocCtx, cancelAlloc := chromedp.NewExecAllocator(context.Background(), opts...)
|
|
defer cancelAlloc()
|
|
|
|
ctx, cancel := chromedp.NewContext(allocCtx)
|
|
defer cancel()
|
|
|
|
ctx, cancelTimeout := context.WithTimeout(ctx, 30*time.Second)
|
|
defer cancelTimeout()
|
|
|
|
dataURI := "data:text/html;base64," + base64.StdEncoding.EncodeToString([]byte(html))
|
|
|
|
var pdfBuf []byte
|
|
err := chromedp.Run(ctx,
|
|
chromedp.Navigate(dataURI),
|
|
chromedp.WaitReady("body", chromedp.ByQuery),
|
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
|
buf, _, err := page.PrintToPDF().
|
|
WithPrintBackground(true).
|
|
WithPreferCSSPageSize(true).
|
|
WithMarginTop(0.4).
|
|
WithMarginBottom(0.4).
|
|
WithMarginLeft(0.4).
|
|
WithMarginRight(0.4).
|
|
Do(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pdfBuf = buf
|
|
return nil
|
|
}),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error generando PDF: %w", err)
|
|
}
|
|
return pdfBuf, nil
|
|
}
|
|
|
|
// SaveGeneratedFile persiste bytes generados (ej. un PDF) en disco bajo uploads/documentos/{subdir}
|
|
// y retorna la ruta relativa guardada en el modelo (DocumentoGenerado.Archivo, ProyectoDocumento.Archivo, etc).
|
|
func SaveGeneratedFile(subdir, filename string, data []byte) (string, error) {
|
|
dir := fmt.Sprintf("uploads/documentos/%s", subdir)
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return "", fmt.Errorf("no se pudo crear el directorio %s: %w", dir, err)
|
|
}
|
|
path := dir + "/" + filename
|
|
if err := os.WriteFile(path, data, 0644); err != nil {
|
|
return "", fmt.Errorf("no se pudo escribir %s: %w", path, err)
|
|
}
|
|
return path, nil
|
|
}
|