Files
soft_usite/rest/controllers/plantilla_documento_controller.go
Lizandro GuarnizoandClaude Opus 5 86cdb2d368 feat(plantillas): ver cómo queda la plantilla mientras se edita
Se editaba HTML a ciegas: había que guardar y generar un documento real para
saber si estaba bien. Ahora el modal tiene un panel de vista previa al lado del
editor, que se actualiza mientras se escribe y se abre solo al editar una
plantilla o al importar una con IA.

Se ejecuta la plantilla de verdad contra datos de ejemplo, no se reemplaza
texto: es la única forma de que {{range .Items}} y los campos anidados se vean
como van a salir, y de que un error de sintaxis aparezca mientras se edita en
vez de al generar el PDF. Los datos de ejemplo salen de DatosBaseDocumento,
igual que en producción, para que la vista previa no muestre una cosa y el
documento otra.

El iframe va en sandbox sin scripts ni same-origin: el HTML lo escribe un
admin, pero no tiene por qué correr con los permisos del panel.

De paso, la ayuda de variables ofrecía {{.Cliente.Nit}}, que no existe en el
modelo — el campo es Documento.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 21:28:06 -05:00

229 lines
7.8 KiB
Go

package controllers
import (
"io"
"log"
"math"
"strconv"
"text/template"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
// ─── Plantillas de documento (cotización, contrato, acta, cuenta de cobro) ────
func PlantillasDocumentoView(c *fiber.Ctx) error {
return c.Render("automatizacion/plantillas_documento", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func TarifasView(c *fiber.Ctx) error {
return c.Render("automatizacion/tarifas", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func GetPlantillasDocumento(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
limit, _ := strconv.Atoi(c.Query("limit", "20"))
if page < 1 {
page = 1
}
offset := (page - 1) * limit
records, total, err := models.GetAllPlantillasDocumento(limit, offset, c.Query("search"), c.Query("tipo"))
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"registros": records,
"total": total,
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
"page": page,
})
}
func GetPlantillaDocumento(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
item, err := models.GetPlantillaDocumentoByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrada"})
}
return c.JSON(item)
}
func CreatePlantillaDocumento(c *fiber.Ctx) error {
var m models.PlantillaDocumento
if err := c.BodyParser(&m); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if m.ContenidoHTML != "" {
if _, err := template.New("validate").Parse(m.ContenidoHTML); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Sintaxis inválida en el HTML: " + err.Error()})
}
}
if err := models.CreatePlantillaDocumento(m); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(fiber.Map{"message": "Plantilla creada", "ok": true})
}
func UpdatePlantillaDocumento(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
var updates map[string]interface{}
if err := c.BodyParser(&updates); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if html, ok := updates["contenido_html"].(string); ok && html != "" {
if _, err := template.New("validate").Parse(html); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Sintaxis inválida en el HTML: " + err.Error()})
}
}
if err := models.UpdatePlantillaDocumento(uint(id), updates); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Actualizado", "ok": true})
}
func DeletePlantillaDocumento(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
if err := models.DeletePlantillaDocumento(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Eliminado", "ok": true})
}
// ImportarPlantillaDocumento recibe un documento (docx, html, txt o imagen), lo
// lee y le pide a la IA que lo devuelva como plantilla HTML con las variables
// del generador. NO guarda nada: el HTML vuelve al editor para revisarlo.
// POST /app/api/plantillas-documento/importar (multipart: archivo, tipo)
func ImportarPlantillaDocumento(c *fiber.Ctx) error {
archivo, err := c.FormFile("archivo")
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Subí un archivo en el campo 'archivo'"})
}
if archivo.Size > 10<<20 {
return c.Status(400).JSON(fiber.Map{"error": "El archivo supera los 10 MB"})
}
f, err := archivo.Open()
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "No se pudo leer el archivo"})
}
defer f.Close()
datos, err := io.ReadAll(io.LimitReader(f, 10<<20))
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "No se pudo leer el archivo"})
}
texto, err := services.ExtraerTextoDePlantilla(archivo.Filename, datos)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
tipo := c.FormValue("tipo", "cotizacion")
html, err := services.ConvertirEnPlantilla(tipo, texto)
if err != nil {
// Al log también: el mensaje del proveedor es lo único que dice por qué
// falló, y desde el navegador se ve recortado.
log.Printf("[PLANTILLAS] importar (%s, %s): %v", tipo, archivo.Filename, err)
return c.Status(422).JSON(fiber.Map{"error": err.Error()})
}
// Si la IA devolvió algo que no compila, es mejor decirlo acá que al guardar.
if _, err := template.New("validate").Parse(html); err != nil {
return c.Status(200).JSON(fiber.Map{
"contenido_html": html,
"aviso": "La IA devolvió HTML con sintaxis de plantilla inválida (" + err.Error() + "). Revisalo antes de guardar.",
})
}
return c.JSON(fiber.Map{"contenido_html": html})
}
// PrevisualizarPlantillaDocumento renderiza la plantilla con datos de ejemplo
// para ver cómo queda antes de guardarla.
// POST /app/api/plantillas-documento/previsualizar {tipo, contenido_html}
func PrevisualizarPlantillaDocumento(c *fiber.Ctx) error {
var req struct {
Tipo string `json:"tipo"`
ContenidoHTML string `json:"contenido_html"`
}
if err := c.BodyParser(&req); err != nil {
return c.Status(400).JSON(fiber.Map{"error": "body inválido"})
}
html, err := services.RenderizarPlantillaEjemplo(req.Tipo, req.ContenidoHTML)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"html": html})
}
// ─── Tarifas ────────────────────────────────────────────────────────────────
func GetTarifas(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
limit, _ := strconv.Atoi(c.Query("limit", "50"))
if page < 1 {
page = 1
}
offset := (page - 1) * limit
records, total, err := models.GetAllTarifas(limit, offset, c.Query("search"), c.Query("categoria"))
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{
"registros": records,
"total": total,
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
"page": page,
})
}
func CreateTarifa(c *fiber.Ctx) error {
var m models.Tarifa
if err := c.BodyParser(&m); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if err := models.CreateTarifa(m); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(fiber.Map{"message": "Tarifa creada", "ok": true})
}
func UpdateTarifa(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
var updates map[string]interface{}
if err := c.BodyParser(&updates); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if err := models.UpdateTarifa(uint(id), updates); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Actualizado", "ok": true})
}
func DeleteTarifa(c *fiber.Ctx) error {
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
}
if err := models.DeleteTarifa(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Eliminado", "ok": true})
}