362 lines
11 KiB
Go
362 lines
11 KiB
Go
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
|
|
PrecioSugerido float64 `json:"precio_sugerido"`
|
|
}
|
|
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"
|
|
}
|
|
var sugerido float64
|
|
for _, s := range r.Servicios {
|
|
sugerido += s.Precio
|
|
}
|
|
dtos[i] = ContratoDTO{Contrato: r, DiasRestantes: dias, Urgencia: urgencia, PrecioSugerido: sugerido}
|
|
}
|
|
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"`
|
|
ServicioIDs []uint `json:"servicio_ids"`
|
|
FechaInicio string `json:"fecha_inicio"`
|
|
FechaVencimiento string `json:"fecha_vencimiento"`
|
|
PrecioAcordado float64 `json:"precio_acordado"`
|
|
AutoRenovar bool `json:"auto_renovar"`
|
|
Notas string `json:"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,
|
|
FechaInicio: fi,
|
|
FechaVencimiento: fv,
|
|
PrecioAcordado: inp.PrecioAcordado,
|
|
AutoRenovar: inp.AutoRenovar,
|
|
Notas: inp.Notas,
|
|
Estado: "activo",
|
|
}
|
|
if err := models.CreateContrato(m, inp.ServicioIDs); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
// Devolver el ID del contrato creado para que el frontend pueda enviar bienvenida
|
|
created, _ := models.GetUltimoContratoByCliente(inp.ClienteID)
|
|
contratoID := uint(0)
|
|
if created != nil {
|
|
contratoID = created.ID
|
|
}
|
|
return c.Status(201).JSON(fiber.Map{"message": "Contrato creado", "ok": true, "id": contratoID})
|
|
}
|
|
|
|
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 {
|
|
ServicioIDs []uint `json:"servicio_ids"`
|
|
Estado string `json:"estado"`
|
|
FechaVencimiento string `json:"fecha_vencimiento"`
|
|
PrecioAcordado float64 `json:"precio_acordado"`
|
|
AutoRenovar bool `json:"auto_renovar"`
|
|
Notas string `json:"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, inp.ServicioIDs); 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"})
|
|
}
|
|
// Usar periodicidad del primer servicio asociado
|
|
periodicidad := ""
|
|
if len(existing.Servicios) > 0 {
|
|
periodicidad = existing.Servicios[0].Periodicidad
|
|
}
|
|
nuevaInicio := existing.FechaVencimiento.AddDate(0, 0, 1)
|
|
nuevaVenc := calcularFechaVencimiento(nuevaInicio, periodicidad)
|
|
|
|
// Copiar IDs de servicios del contrato anterior
|
|
var servicioIDs []uint
|
|
for _, s := range existing.Servicios {
|
|
servicioIDs = append(servicioIDs, s.ID)
|
|
}
|
|
nuevo := models.Contrato{
|
|
ClienteID: existing.ClienteID,
|
|
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, servicioIDs); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
// Marcar anterior como renovado
|
|
existing.Estado = "renovado"
|
|
models.UpdateContrato(*existing, nil)
|
|
|
|
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"})
|
|
}
|
|
|
|
// Si se pasa regla_id, usar esa regla específica (EnviarNotificacionGrupo)
|
|
var body struct {
|
|
ReglaID uint `json:"regla_id"`
|
|
}
|
|
_ = c.BodyParser(&body)
|
|
|
|
if body.ReglaID > 0 {
|
|
regla, err := models.GetReglaByID(body.ReglaID)
|
|
if err != nil {
|
|
return c.Status(404).JSON(fiber.Map{"error": "Regla no encontrada"})
|
|
}
|
|
if err := services.EnviarNotificacionGrupo(regla, &contrato.Cliente, []models.Contrato{*contrato}); err != nil {
|
|
return c.Status(500).JSON(fiber.Map{"error": err.Error()})
|
|
}
|
|
return c.JSON(fiber.Map{"message": "Correo enviado", "ok": true})
|
|
}
|
|
|
|
// Sin regla_id → flujo manual genérico
|
|
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})
|
|
}
|
|
|
|
// GetHistorialContrato devuelve la línea de tiempo de actividad de un contrato
|
|
func GetHistorialContrato(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"})
|
|
}
|
|
|
|
notifLogs, _ := models.GetLogsByContratoID(uint(id))
|
|
pagoLogs, _ := models.GetDispatchLogsByContratoID(uint(id))
|
|
|
|
// Construir timeline unificado
|
|
type Evento struct {
|
|
Tipo string `json:"tipo"` // creacion | renovacion | notificacion | pago
|
|
Icono string `json:"icono"`
|
|
Titulo string `json:"titulo"`
|
|
Detalle string `json:"detalle"`
|
|
Estado string `json:"estado"` // ok | error | info
|
|
FechaISO string `json:"fecha"`
|
|
}
|
|
|
|
var timeline []Evento
|
|
|
|
// Evento: creación del contrato
|
|
timeline = append(timeline, Evento{
|
|
Tipo: "creacion",
|
|
Icono: "document",
|
|
Titulo: "Contrato creado",
|
|
Detalle: "Inicio: " + contrato.FechaInicio.Format("02/01/2006") + " · Vence: " + contrato.FechaVencimiento.Format("02/01/2006"),
|
|
Estado: "info",
|
|
FechaISO: contrato.CreatedAt.Format(time.RFC3339),
|
|
})
|
|
|
|
// Evento: renovaciones (updated_at con estado renovado — heurístico por estado)
|
|
if contrato.Estado == "renovado" {
|
|
timeline = append(timeline, Evento{
|
|
Tipo: "renovacion",
|
|
Icono: "refresh",
|
|
Titulo: "Contrato renovado",
|
|
Detalle: "Nuevo vencimiento: " + contrato.FechaVencimiento.Format("02/01/2006"),
|
|
Estado: "ok",
|
|
FechaISO: contrato.UpdatedAt.Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// Eventos: notificaciones enviadas
|
|
for _, n := range notifLogs {
|
|
estado := "ok"
|
|
if n.Estado == "fallido" {
|
|
estado = "error"
|
|
} else if n.Estado == "pendiente" {
|
|
estado = "info"
|
|
}
|
|
detalle := n.Asunto
|
|
if n.Estado == "fallido" && n.ErrorMsg != "" {
|
|
detalle += " · Error: " + n.ErrorMsg
|
|
}
|
|
reglaLabel := ""
|
|
if n.Regla.ID > 0 {
|
|
reglaLabel = " (" + n.Regla.Nombre + ")"
|
|
}
|
|
timeline = append(timeline, Evento{
|
|
Tipo: "notificacion",
|
|
Icono: "mail",
|
|
Titulo: "Notificación enviada" + reglaLabel,
|
|
Detalle: detalle,
|
|
Estado: estado,
|
|
FechaISO: n.CreatedAt.Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// Eventos: pagos / dispatches
|
|
for _, p := range pagoLogs {
|
|
estado := "ok"
|
|
if p.Estado == "failed" {
|
|
estado = "error"
|
|
}
|
|
detalle := p.Referencia
|
|
if p.PayerEmail != "" {
|
|
detalle += " · " + p.PayerEmail
|
|
}
|
|
if p.Fuente != "" {
|
|
detalle += " · " + p.Fuente
|
|
}
|
|
timeline = append(timeline, Evento{
|
|
Tipo: "pago",
|
|
Icono: "currency",
|
|
Titulo: "Pago recibido",
|
|
Detalle: detalle,
|
|
Estado: estado,
|
|
FechaISO: p.CreatedAt.Format(time.RFC3339),
|
|
})
|
|
}
|
|
|
|
// Ordenar por fecha desc
|
|
for i := 0; i < len(timeline)-1; i++ {
|
|
for j := i + 1; j < len(timeline); j++ {
|
|
if timeline[j].FechaISO > timeline[i].FechaISO {
|
|
timeline[i], timeline[j] = timeline[j], timeline[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
return c.JSON(fiber.Map{
|
|
"contrato": fiber.Map{
|
|
"id": contrato.ID,
|
|
"cliente": contrato.Cliente.Nombre,
|
|
"estado": contrato.Estado,
|
|
},
|
|
"timeline": timeline,
|
|
"total": len(timeline),
|
|
})
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|