feat: automatización de cotizaciones, contratos, actas y cuentas de cobro con IA
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>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
772a4a5656
commit
516220a8a1
@@ -0,0 +1,56 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
)
|
||||
|
||||
// GetArquitecturas lista los patrones de arquitectura de referencia.
|
||||
func GetArquitecturas(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.GetAllArquitecturas(limit, offset, c.Query("search"), c.Query("solo_referencias") == "true")
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
type crearArquitecturaRequest struct {
|
||||
Requerimiento string `json:"requerimiento"`
|
||||
Propuesta string `json:"propuesta"`
|
||||
Nombre string `json:"nombre"`
|
||||
ClienteID uint `json:"cliente_id"`
|
||||
GuardarComoReferencia bool `json:"guardar_como_referencia"`
|
||||
}
|
||||
|
||||
// CreateArquitectura genera el PDF de una propuesta técnica.
|
||||
// POST /api/v2/arquitecturas
|
||||
func CreateArquitectura(c *fiber.Ctx) error {
|
||||
var req crearArquitecturaRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
var clienteID *uint
|
||||
if req.ClienteID != 0 {
|
||||
clienteID = &req.ClienteID
|
||||
}
|
||||
doc, err := services.GenerarArquitectura(req.Requerimiento, req.Propuesta, req.Nombre, clienteID, req.GuardarComoReferencia, generadoPorFromContext(c))
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.Status(201).JSON(fiber.Map{"ok": true, "documento": doc})
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
)
|
||||
|
||||
// AsistenteView renderiza el chat propio dentro del dashboard: mismo motor y
|
||||
// mismas tools que el bot de Telegram (services.ProcessAgentMessage), solo que
|
||||
// accesible directamente desde la web sin pasar por Telegram.
|
||||
func AsistenteView(c *fiber.Ctx) error {
|
||||
return c.Render("automatizacion/asistente", fiber.Map{
|
||||
"user": c.Locals("user"),
|
||||
"modules": c.Locals("userModules"),
|
||||
}, "layouts/main")
|
||||
}
|
||||
|
||||
type asistenteChatRequest struct {
|
||||
Mensaje string `json:"mensaje"`
|
||||
}
|
||||
|
||||
// asistenteChatID deriva un chat_id sintético y estable por usuario del dashboard,
|
||||
// en un rango que nunca colisiona con IDs reales de chats de Telegram (positivos
|
||||
// < 10^10 para chats privados, negativos para grupos/canales).
|
||||
func asistenteChatID(c *fiber.Ctx) int64 {
|
||||
user, _ := c.Locals("user").(map[string]interface{})
|
||||
var userID uint
|
||||
if v, ok := user["ID"]; ok {
|
||||
switch x := v.(type) {
|
||||
case uint:
|
||||
userID = x
|
||||
case int:
|
||||
userID = uint(x)
|
||||
case float64:
|
||||
userID = uint(x)
|
||||
}
|
||||
}
|
||||
return 9_000_000_000 + int64(userID)
|
||||
}
|
||||
|
||||
// PostAsistenteChat procesa un mensaje del chat propio del dashboard.
|
||||
// POST /app/api/asistente/chat
|
||||
func PostAsistenteChat(c *fiber.Ctx) error {
|
||||
var req asistenteChatRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
if req.Mensaje == "" {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "mensaje requerido"})
|
||||
}
|
||||
|
||||
ai, err := models.GetAgenteBotAiConfig()
|
||||
if err != nil {
|
||||
return c.Status(503).JSON(fiber.Map{"error": fmt.Sprintf("Asistente no configurado: %v", err)})
|
||||
}
|
||||
|
||||
respuesta, err := services.ProcessAgentMessage(asistenteChatID(c), req.Mensaje, ai)
|
||||
if err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true, "respuesta": respuesta})
|
||||
}
|
||||
|
||||
// DeleteAsistenteHistorial borra el historial de la conversación del usuario actual.
|
||||
// DELETE /app/api/asistente/historial
|
||||
func DeleteAsistenteHistorial(c *fiber.Ctx) error {
|
||||
if err := models.ClearAgentHistory(asistenteChatID(c)); err != nil {
|
||||
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
)
|
||||
|
||||
// ─── Render de vistas ────────────────────────────────────────────────────────
|
||||
@@ -408,12 +409,12 @@ func GetCuentasCobro(c *fiber.Ctx) error {
|
||||
|
||||
func CreateCuentaCobro(c *fiber.Ctx) error {
|
||||
type Req struct {
|
||||
ClienteID uint `json:"cliente_id"`
|
||||
Fecha string `json:"fecha"`
|
||||
Descripcion string `json:"descripcion"`
|
||||
Valor float64 `json:"valor"`
|
||||
FechaVencimiento string `json:"fecha_vencimiento"`
|
||||
Notas string `json:"notas"`
|
||||
ClienteID uint `json:"cliente_id"`
|
||||
Fecha string `json:"fecha"`
|
||||
Descripcion string `json:"descripcion"`
|
||||
Valor float64 `json:"valor"`
|
||||
FechaVencimiento string `json:"fecha_vencimiento"`
|
||||
Notas string `json:"notas"`
|
||||
}
|
||||
var req Req
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
@@ -452,13 +453,13 @@ func UpdateCuentaCobro(c *fiber.Ctx) error {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||
}
|
||||
type Req struct {
|
||||
Estado string `json:"estado"`
|
||||
FechaPago string `json:"fecha_pago"`
|
||||
TransaccionID *uint `json:"transaccion_id"`
|
||||
Notas string `json:"notas"`
|
||||
FechaVencimiento string `json:"fecha_vencimiento"`
|
||||
Descripcion string `json:"descripcion"`
|
||||
Valor float64 `json:"valor"`
|
||||
Estado string `json:"estado"`
|
||||
FechaPago string `json:"fecha_pago"`
|
||||
TransaccionID *uint `json:"transaccion_id"`
|
||||
Notas string `json:"notas"`
|
||||
FechaVencimiento string `json:"fecha_vencimiento"`
|
||||
Descripcion string `json:"descripcion"`
|
||||
Valor float64 `json:"valor"`
|
||||
}
|
||||
var req Req
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
@@ -564,10 +565,10 @@ func UpdateCuentaPagar(c *fiber.Ctx) error {
|
||||
return c.Status(400).JSON(fiber.Map{"error": "ID inválido"})
|
||||
}
|
||||
type Req struct {
|
||||
Estado string `json:"estado"`
|
||||
FechaPago string `json:"fecha_pago"`
|
||||
TransaccionID *uint `json:"transaccion_id"`
|
||||
Notas string `json:"notas"`
|
||||
Estado string `json:"estado"`
|
||||
FechaPago string `json:"fecha_pago"`
|
||||
TransaccionID *uint `json:"transaccion_id"`
|
||||
Notas string `json:"notas"`
|
||||
}
|
||||
var req Req
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
@@ -614,6 +615,21 @@ func MarcarCuentaPagarPagada(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{"ok": true})
|
||||
}
|
||||
|
||||
// GenerarDocumentoCuentaCobro produce el PDF de solicitud de cuenta de cobro a
|
||||
// partir de la plantilla activa tipo 'cuenta_cobro'.
|
||||
// POST /api/v2/contabilidad/cuentas-cobro/:id/generar-documento
|
||||
func GenerarDocumentoCuentaCobro(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"})
|
||||
}
|
||||
doc, err := services.GenerarDocumentoCuentaCobro(uint(id), generadoPorFromContext(c))
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.Status(201).JSON(fiber.Map{"ok": true, "documento": doc})
|
||||
}
|
||||
|
||||
func DeleteCuentaPagar(c *fiber.Ctx) error {
|
||||
id, err := strconv.ParseUint(c.Params("id"), 10, 32)
|
||||
if err != nil {
|
||||
|
||||
@@ -462,3 +462,19 @@ func MarcarPagoManual(c *fiber.Ctx) error {
|
||||
go services.EnviarCorreoConfirmacionPago(uint(id), "manual")
|
||||
return c.JSON(fiber.Map{"ok": true, "mensaje": "Pago marcado como confirmado"})
|
||||
}
|
||||
|
||||
// GenerarDocumentoContrato produce el PDF del contrato (cláusulas estándar) a partir
|
||||
// de la plantilla activa tipo 'contrato'. La lógica real vive en
|
||||
// services.GenerarDocumentoContrato, compartida con la tool del agente.
|
||||
// POST /api/v2/contratos/:id/generar-documento
|
||||
func GenerarDocumentoContrato(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"})
|
||||
}
|
||||
doc, err := services.GenerarDocumentoContrato(uint(id), generadoPorFromContext(c))
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.Status(201).JSON(fiber.Map{"ok": true, "documento": doc})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||
)
|
||||
|
||||
type crearCotizacionRequest struct {
|
||||
ClienteID uint `json:"cliente_id"`
|
||||
Alcance string `json:"alcance"`
|
||||
TipoProyecto string `json:"tipo_proyecto"`
|
||||
Items []services.ItemCotizacion `json:"items"`
|
||||
}
|
||||
|
||||
// CreateCotizacion genera el PDF de una cotización a partir de la plantilla activa
|
||||
// tipo 'cotizacion' + los items ya calculados por quien llama (Claude decide los
|
||||
// valores consultando /api/v2/tarifas, el backend solo arma y suma el documento).
|
||||
// La lógica real vive en services.CrearCotizacion, compartida con la tool del agente.
|
||||
// POST /api/v2/cotizaciones
|
||||
func CreateCotizacion(c *fiber.Ctx) error {
|
||||
var req crearCotizacionRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
doc, total, err := services.CrearCotizacion(req.ClienteID, req.Alcance, req.TipoProyecto, req.Items, generadoPorFromContext(c))
|
||||
if err != nil {
|
||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.Status(201).JSON(fiber.Map{
|
||||
"ok": true,
|
||||
"message": fmt.Sprintf("Cotización #%d generada", doc.ID),
|
||||
"total": total,
|
||||
"documento": doc,
|
||||
})
|
||||
}
|
||||
|
||||
// generadoPorFromContext distingue si la petición vino del dashboard (sesión web,
|
||||
// AuthApi) o de la API admin/agente (AdminApiAuth con ADMIN_API_KEY / bot).
|
||||
func generadoPorFromContext(c *fiber.Ctx) string {
|
||||
if c.Get("X-Agent-Channel") == "telegram" {
|
||||
return "telegram"
|
||||
}
|
||||
if c.Get("Authorization") != "" || c.Get("X-API-Key") != "" {
|
||||
return "claude_api"
|
||||
}
|
||||
return "web"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
)
|
||||
|
||||
// GetDocumentosGenerados lista el historial de documentos producidos por la
|
||||
// automatización con IA (cotizaciones, contratos, actas, cuentas de cobro),
|
||||
// sin importar el canal que los generó.
|
||||
func GetDocumentosGenerados(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
|
||||
clienteID, _ := strconv.ParseUint(c.Query("cliente_id", "0"), 10, 32)
|
||||
records, total, err := models.GetAllDocumentosGenerados(limit, offset, c.Query("tipo"), uint(clienteID))
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
// DownloadDocumentoGenerado sirve el PDF ya generado.
|
||||
func DownloadDocumentoGenerado(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"})
|
||||
}
|
||||
doc, err := models.GetDocumentoGeneradoByID(uint(id))
|
||||
if err != nil || doc.Archivo == "" {
|
||||
return c.Status(404).JSON(fiber.Map{"error": "Documento no encontrado"})
|
||||
}
|
||||
clean := filepath.Clean(doc.Archivo)
|
||||
if !strings.HasPrefix(clean, "uploads/") {
|
||||
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
|
||||
}
|
||||
c.Set("Content-Disposition", fmt.Sprintf(`inline; filename="%s"`, doc.Nombre))
|
||||
return c.SendFile(clean)
|
||||
}
|
||||
@@ -143,6 +143,7 @@ func AdminApiSpec(c *fiber.Ctx) error {
|
||||
{"method": "GET", "path": "/api/v2/contratos/:id/historial", "desc": "Historial del contrato"},
|
||||
{"method": "POST", "path": "/api/v2/contratos/:id/verificar-pago", "desc": "Verificar pago Bold"},
|
||||
{"method": "POST", "path": "/api/v2/contratos/:id/marcar-pagado", "desc": "Marcar pago manual"},
|
||||
{"method": "POST", "path": "/api/v2/contratos/:id/generar-documento", "desc": "Genera el PDF del contrato (cláusulas estándar) desde la plantilla activa tipo 'contrato'"},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -290,6 +291,7 @@ func AdminApiSpec(c *fiber.Ctx) error {
|
||||
{"method": "POST", "path": "/api/v2/contabilidad/cuentas-cobro", "desc": "Crear cuenta por cobrar", "body": "cliente_id, descripcion, valor, fecha, fecha_vencimiento, notas"},
|
||||
{"method": "PUT", "path": "/api/v2/contabilidad/cuentas-cobro/:id", "desc": "Actualizar cuenta por cobrar"},
|
||||
{"method": "DELETE", "path": "/api/v2/contabilidad/cuentas-cobro/:id", "desc": "Eliminar cuenta por cobrar"},
|
||||
{"method": "POST", "path": "/api/v2/contabilidad/cuentas-cobro/:id/generar-documento", "desc": "Genera el PDF de solicitud de cuenta de cobro desde la plantilla activa tipo 'cuenta_cobro'"},
|
||||
{"method": "GET", "path": "/api/v2/contabilidad/cuentas-pagar", "desc": "Listar cuentas por pagar. Query: ?page=&search=&estado="},
|
||||
{"method": "POST", "path": "/api/v2/contabilidad/cuentas-pagar", "desc": "Crear cuenta por pagar", "body": "entidad_id, descripcion, valor, fecha, vencimiento, notas"},
|
||||
{"method": "PUT", "path": "/api/v2/contabilidad/cuentas-pagar/:id", "desc": "Actualizar cuenta por pagar"},
|
||||
@@ -494,6 +496,45 @@ func AdminApiSpec(c *fiber.Ctx) error {
|
||||
{"method": "GET", "path": "/api/v2/vcard-api/miniwebs", "desc": "Listar miniwebs"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nombre": "Automatización IA: Plantillas de documento",
|
||||
"endpoints": []fiber.Map{
|
||||
{"method": "GET", "path": "/api/v2/plantillas-documento", "desc": "Listar plantillas. Query: ?tipo=cotizacion|contrato|acta|cuenta_cobro&search="},
|
||||
{"method": "GET", "path": "/api/v2/plantillas-documento/:id", "desc": "Detalle de una plantilla"},
|
||||
{"method": "POST", "path": "/api/v2/plantillas-documento", "desc": "Crear plantilla", "body": "tipo, nombre, contenido_html, version, activa"},
|
||||
{"method": "PUT", "path": "/api/v2/plantillas-documento/:id", "desc": "Actualizar plantilla"},
|
||||
{"method": "DELETE", "path": "/api/v2/plantillas-documento/:id", "desc": "Eliminar plantilla"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nombre": "Automatización IA: Tarifas",
|
||||
"endpoints": []fiber.Map{
|
||||
{"method": "GET", "path": "/api/v2/tarifas", "desc": "Listar tarifas. Query: ?categoria=hora_servicio|licencia|vm_azure|margen|otro&search="},
|
||||
{"method": "POST", "path": "/api/v2/tarifas", "desc": "Crear tarifa", "body": "categoria, nombre, valor, moneda, unidad, notas"},
|
||||
{"method": "PUT", "path": "/api/v2/tarifas/:id", "desc": "Actualizar tarifa"},
|
||||
{"method": "DELETE", "path": "/api/v2/tarifas/:id", "desc": "Eliminar tarifa"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nombre": "Automatización IA: Arquitecturas",
|
||||
"endpoints": []fiber.Map{
|
||||
{"method": "GET", "path": "/api/v2/arquitecturas", "desc": "Listar patrones de arquitectura de referencia. Query: ?search=&solo_referencias=true"},
|
||||
{"method": "POST", "path": "/api/v2/arquitecturas", "desc": "Genera el PDF de una propuesta técnica ya redactada", "body": "requerimiento, propuesta, nombre, cliente_id, guardar_como_referencia"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nombre": "Automatización IA: Cotizaciones",
|
||||
"endpoints": []fiber.Map{
|
||||
{"method": "POST", "path": "/api/v2/cotizaciones", "desc": "Genera el PDF de una cotización desde la plantilla activa. Consultar /api/v2/tarifas antes para calcular los items", "body": "cliente_id, alcance, tipo_proyecto, items: [{descripcion, cantidad, valor_unitario, unidad}]"},
|
||||
},
|
||||
},
|
||||
{
|
||||
"nombre": "Automatización IA: Historial de documentos generados",
|
||||
"endpoints": []fiber.Map{
|
||||
{"method": "GET", "path": "/api/v2/documentos-generados", "desc": "Historial de documentos generados. Query: ?tipo=&cliente_id="},
|
||||
{"method": "GET", "path": "/api/v2/documentos-generados/:id/download", "desc": "Descargar el PDF generado"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
return c.JSON(spec)
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"text/template"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||
)
|
||||
|
||||
// ─── 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})
|
||||
}
|
||||
|
||||
// ─── 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})
|
||||
}
|
||||
Reference in New Issue
Block a user