This commit is contained in:
Lizandro Guarnizo
2026-04-29 23:36:30 -05:00
parent 9d4c4dba65
commit f787821444
30 changed files with 3738 additions and 8 deletions
+83
View File
@@ -0,0 +1,83 @@
package controllers
import (
"math"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
func ClientesView(c *fiber.Ctx) error {
return c.Render("renovaciones/clientes", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func GetClientes(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
limit, _ := strconv.Atoi(c.Query("limit", "10"))
if page < 1 {
page = 1
}
offset := (page - 1) * limit
records, total, err := models.GetAllClientes(limit, offset, c.Query("search"))
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,
"limit": limit,
})
}
func GetClientesSelect(c *fiber.Ctx) error {
records, err := models.GetAllClientesSelect()
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(records)
}
func CreateCliente(c *fiber.Ctx) error {
var m models.Cliente
if err := c.BodyParser(&m); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
m.Activo = true
if err := models.CreateCliente(m); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(fiber.Map{"message": "Cliente creado", "ok": true})
}
func UpdateCliente(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 m models.Cliente
if err := c.BodyParser(&m); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
m.ID = uint(id)
if err := models.UpdateCliente(m); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Actualizado", "ok": true})
}
func DeleteCliente(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.DeleteCliente(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Eliminado", "ok": true})
}
+206
View File
@@ -0,0 +1,206 @@
package controllers
import (
"math"
"strconv"
"time"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
)
func ContratosView(c *fiber.Ctx) error {
return c.Render("renovaciones/contratos", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func GetContratos(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
limit, _ := strconv.Atoi(c.Query("limit", "10"))
if page < 1 {
page = 1
}
offset := (page - 1) * limit
records, total, err := models.GetAllContratos(limit, offset, c.Query("search"), c.Query("estado"))
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
// Enriquecer con días restantes
type ContratoDTO struct {
models.Contrato
DiasRestantes int `json:"dias_restantes"`
Urgencia string `json:"urgencia"` // verde | amarillo | rojo
}
dtos := make([]ContratoDTO, len(records))
now := time.Now()
for i, r := range records {
dias := int(r.FechaVencimiento.Sub(now).Hours() / 24)
urgencia := "verde"
if dias <= 0 {
urgencia = "rojo"
} else if dias <= 30 {
urgencia = "amarillo"
}
dtos[i] = ContratoDTO{Contrato: r, DiasRestantes: dias, Urgencia: urgencia}
}
return c.JSON(fiber.Map{
"registros": dtos,
"total": total,
"totalPages": int(math.Ceil(float64(total) / float64(limit))),
"page": page,
"limit": limit,
})
}
func CreateContrato(c *fiber.Ctx) error {
type Input struct {
ClienteID uint `json:"cliente_id" form:"cliente_id"`
ServicioID uint `json:"servicio_id" form:"servicio_id"`
FechaInicio string `json:"fecha_inicio" form:"fecha_inicio"`
FechaVencimiento string `json:"fecha_vencimiento" form:"fecha_vencimiento"`
PrecioAcordado float64 `json:"precio_acordado" form:"precio_acordado"`
AutoRenovar bool `json:"auto_renovar" form:"auto_renovar"`
Notas string `json:"notas" form:"notas"`
}
var inp Input
if err := c.BodyParser(&inp); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
fi, err := time.Parse("2006-01-02", inp.FechaInicio)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Fecha inicio inválida"})
}
fv, err := time.Parse("2006-01-02", inp.FechaVencimiento)
if err != nil {
return c.Status(400).JSON(fiber.Map{"error": "Fecha vencimiento inválida"})
}
m := models.Contrato{
ClienteID: inp.ClienteID,
ServicioID: inp.ServicioID,
FechaInicio: fi,
FechaVencimiento: fv,
PrecioAcordado: inp.PrecioAcordado,
AutoRenovar: inp.AutoRenovar,
Notas: inp.Notas,
Estado: "activo",
}
if err := models.CreateContrato(m); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(fiber.Map{"message": "Contrato creado", "ok": true})
}
func UpdateContrato(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"})
}
type Input struct {
Estado string `json:"estado" form:"estado"`
FechaVencimiento string `json:"fecha_vencimiento" form:"fecha_vencimiento"`
PrecioAcordado float64 `json:"precio_acordado" form:"precio_acordado"`
AutoRenovar bool `json:"auto_renovar" form:"auto_renovar"`
Notas string `json:"notas" form:"notas"`
}
var inp Input
if err := c.BodyParser(&inp); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
existing, err := models.GetContratoByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
}
if inp.FechaVencimiento != "" {
fv, err := time.Parse("2006-01-02", inp.FechaVencimiento)
if err == nil {
existing.FechaVencimiento = fv
}
}
if inp.Estado != "" {
existing.Estado = inp.Estado
}
existing.PrecioAcordado = inp.PrecioAcordado
existing.AutoRenovar = inp.AutoRenovar
existing.Notas = inp.Notas
if err := models.UpdateContrato(*existing); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Actualizado", "ok": true})
}
func RenovarContrato(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"})
}
existing, err := models.GetContratoByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
}
// Calcular nueva fecha según periodicidad del servicio
nuevaInicio := existing.FechaVencimiento.AddDate(0, 0, 1)
nuevaVenc := calcularFechaVencimiento(nuevaInicio, existing.Servicio.Periodicidad)
nuevo := models.Contrato{
ClienteID: existing.ClienteID,
ServicioID: existing.ServicioID,
FechaInicio: nuevaInicio,
FechaVencimiento: nuevaVenc,
PrecioAcordado: existing.PrecioAcordado,
AutoRenovar: existing.AutoRenovar,
Estado: "activo",
Notas: "Renovación automática desde contrato #" + strconv.Itoa(int(existing.ID)),
}
if err := models.CreateContrato(nuevo); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
// Marcar anterior como renovado
existing.Estado = "renovado"
models.UpdateContrato(*existing)
return c.JSON(fiber.Map{"message": "Renovado", "ok": true})
}
func EnviarCorreoContrato(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"})
}
contrato, err := models.GetContratoByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
}
if err := services.EnviarCorreoManual(contrato); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Correo enviado", "ok": true})
}
func DeleteContrato(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.DeleteContrato(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Eliminado", "ok": true})
}
func calcularFechaVencimiento(desde time.Time, periodicidad string) time.Time {
switch periodicidad {
case "mensual":
return desde.AddDate(0, 1, 0)
case "trimestral":
return desde.AddDate(0, 3, 0)
case "semestral":
return desde.AddDate(0, 6, 0)
case "anual":
return desde.AddDate(1, 0, 0)
default:
return desde.AddDate(1, 0, 0)
}
}
+222
View File
@@ -0,0 +1,222 @@
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"
)
// ─── Plantillas ─────────────────────────────────────────────────────────────
func PlantillasView(c *fiber.Ctx) error {
return c.Render("renovaciones/plantillas", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func GetPlantillas(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
limit, _ := strconv.Atoi(c.Query("limit", "10"))
if page < 1 {
page = 1
}
offset := (page - 1) * limit
records, total, err := models.GetAllPlantillas(limit, offset, c.Query("search"))
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 CreatePlantilla(c *fiber.Ctx) error {
var m models.PlantillaCorreo
if err := c.BodyParser(&m); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if err := models.CreatePlantilla(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 UpdatePlantilla(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.UpdatePlantilla(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 DeletePlantilla(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.DeletePlantilla(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Eliminado", "ok": true})
}
func PreviewPlantilla(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"})
}
p, err := models.GetPlantillaByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrada"})
}
html, err := services.RenderPlantilla(p, services.DatosEjemplo())
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"html": html, "ok": true})
}
func TestEnvioPlantilla(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 body struct {
Email string `json:"email"`
}
if err := c.BodyParser(&body); err != nil || body.Email == "" {
return c.Status(400).JSON(fiber.Map{"error": "Email requerido"})
}
p, err := models.GetPlantillaByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrada"})
}
if err := services.EnviarCorreoPrueba(body.Email, p); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Correo enviado", "ok": true})
}
// ─── Reglas ──────────────────────────────────────────────────────────────────
func ReglasView(c *fiber.Ctx) error {
return c.Render("renovaciones/reglas", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func GetReglas(c *fiber.Ctx) error {
records, err := models.GetAllReglas()
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"registros": records, "total": len(records)})
}
func CreateRegla(c *fiber.Ctx) error {
var m models.NotificacionRegla
if err := c.BodyParser(&m); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
if err := models.CreateRegla(m); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(fiber.Map{"message": "Regla creada", "ok": true})
}
func UpdateRegla(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 m models.NotificacionRegla
if err := c.BodyParser(&m); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
m.ID = uint(id)
if err := models.UpdateRegla(m); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Actualizado", "ok": true})
}
func DeleteRegla(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.DeleteRegla(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Eliminado", "ok": true})
}
// ─── Historial ───────────────────────────────────────────────────────────────
func HistorialView(c *fiber.Ctx) error {
return c.Render("renovaciones/historial", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func GetHistorial(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.GetAllLogs(limit, offset)
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 ReenviarNotificacion(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"})
}
log, err := models.GetLogByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
}
if err := services.ReenviarLog(log); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Reenviado", "ok": true})
}
func VerPreviewLog(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"})
}
log, err := models.GetLogByID(uint(id))
if err != nil {
return c.Status(404).JSON(fiber.Map{"error": "No encontrado"})
}
return c.JSON(fiber.Map{"html": log.PreviewHTML, "asunto": log.Asunto, "ok": true})
}
+83
View File
@@ -0,0 +1,83 @@
package controllers
import (
"math"
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
func ServiciosView(c *fiber.Ctx) error {
return c.Render("renovaciones/servicios", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
}, "layouts/main")
}
func GetServicios(c *fiber.Ctx) error {
page, _ := strconv.Atoi(c.Query("page", "1"))
limit, _ := strconv.Atoi(c.Query("limit", "10"))
if page < 1 {
page = 1
}
offset := (page - 1) * limit
records, total, err := models.GetAllServicios(limit, offset, c.Query("search"))
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,
"limit": limit,
})
}
func GetServiciosSelect(c *fiber.Ctx) error {
records, err := models.GetAllServiciosSelect()
if err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(records)
}
func CreateServicio(c *fiber.Ctx) error {
var m models.Servicio
if err := c.BodyParser(&m); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
m.Activo = true
if err := models.CreateServicio(m); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(fiber.Map{"message": "Servicio creado", "ok": true})
}
func UpdateServicio(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 m models.Servicio
if err := c.BodyParser(&m); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
m.ID = uint(id)
if err := models.UpdateServicio(m); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Actualizado", "ok": true})
}
func DeleteServicio(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.DeleteServicio(uint(id)); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Eliminado", "ok": true})
}
@@ -0,0 +1,94 @@
package controllers
import (
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/app"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
"github.com/sujit-baniya/fiber-boilerplate/utils"
)
func SmtpConfigView(c *fiber.Ctx) error {
cfg, _ := models.GetSmtpConfig()
return c.Render("renovaciones/smtp", fiber.Map{
"user": c.Locals("user"),
"modules": c.Locals("userModules"),
"config": cfg,
}, "layouts/main")
}
func GetSmtpConfig(c *fiber.Ctx) error {
cfg, err := models.GetSmtpConfig()
if err != nil {
return c.JSON(fiber.Map{"ok": false, "config": nil})
}
// No exponer contraseña en claro
cfg.Password = "***"
return c.JSON(fiber.Map{"ok": true, "config": cfg})
}
func SaveSmtpConfig(c *fiber.Ctx) error {
type Input struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
Encryption string `json:"encryption"`
FromAddress string `json:"from_address"`
FromName string `json:"from_name"`
}
var inp Input
if err := c.BodyParser(&inp); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
// Cifrar contraseña solo si se provee una nueva (no es placeholder)
passEncrypted := inp.Password
if inp.Password != "" && inp.Password != "***" {
passEncrypted = utils.Encrypt(inp.Password, app.Http.Server.Key)
} else if inp.Password == "***" {
// Mantener contraseña ya guardada
if existing, err := models.GetSmtpConfig(); err == nil {
passEncrypted = existing.Password
}
}
cfg := models.SmtpConfig{
Host: inp.Host,
Port: inp.Port,
Username: inp.Username,
Password: passEncrypted,
Encryption: inp.Encryption,
FromAddress: inp.FromAddress,
FromName: inp.FromName,
}
if err := models.SaveSmtpConfig(cfg); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
// Recargar el mailer en memoria con la nueva config
app.Http.Mail.Host = cfg.Host
app.Http.Mail.Port = cfg.Port
app.Http.Mail.Username = cfg.Username
app.Http.Mail.Password = utils.Decrypt(passEncrypted, app.Http.Server.Key)
app.Http.Mail.Encryption = cfg.Encryption
app.Http.Mail.FromAddress = cfg.FromAddress
app.Http.Mail.FromName = cfg.FromName
app.Http.Mail.SetupMailer()
return c.JSON(fiber.Map{"message": "Configuración guardada", "ok": true})
}
func TestSmtpConfig(c *fiber.Ctx) error {
var body struct {
Email string `json:"email"`
}
if err := c.BodyParser(&body); err != nil || body.Email == "" {
return c.Status(400).JSON(fiber.Map{"error": "Email de destino requerido"})
}
htmlBody := "<h2>Test SMTP</h2><p>La configuración SMTP funciona correctamente.</p>"
if err := app.Http.Mail.Send(body.Email, "Test SMTP - U-site", htmlBody); err != nil {
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
}
return c.JSON(fiber.Map{"message": "Correo de prueba enviado", "ok": true})
}