package controllers import ( "fmt" "math" "path/filepath" "strconv" "strings" "time" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) // ─── Render de vistas ──────────────────────────────────────────────────────── func ContabilidadIndex(c *fiber.Ctx) error { return c.Render("contabilidad/contabilidad", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main") } func ContabilidadTransaccionesView(c *fiber.Ctx) error { return c.Render("contabilidad/transacciones", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main") } func ContabilidadCuentasView(c *fiber.Ctx) error { return c.Render("contabilidad/cuentas", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main") } func ContabilidadEntidadesView(c *fiber.Ctx) error { return c.Render("contabilidad/entidades", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main") } func ContabilidadCobroView(c *fiber.Ctx) error { return c.Render("contabilidad/cuentas_cobro", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main") } func ContabilidadPagarView(c *fiber.Ctx) error { return c.Render("contabilidad/cuentas_pagar", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main") } // ─── Dashboard ─────────────────────────────────────────────────────────────── func ContabilidadDashboard(c *fiber.Ctx) error { now := time.Now() mes, _ := strconv.Atoi(c.Query("mes", strconv.Itoa(int(now.Month())))) anio, _ := strconv.Atoi(c.Query("anio", strconv.Itoa(now.Year()))) if mes < 1 || mes > 12 { mes = int(now.Month()) } if anio < 2000 { anio = now.Year() } data, err := models.GetDashboardData(mes, anio) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(data) } func ContabilidadConsolidado(ctx *fiber.Ctx) error { now := time.Now() mes, _ := strconv.Atoi(ctx.Query("mes", strconv.Itoa(int(now.Month())))) anio, _ := strconv.Atoi(ctx.Query("anio", strconv.Itoa(now.Year()))) if mes < 1 || mes > 12 { mes = int(now.Month()) } if anio < 2000 { anio = now.Year() } data, err := models.CalcularYGuardarConsolidado(mes, anio) if err != nil { return ctx.Status(500).JSON(fiber.Map{"error": err.Error()}) } return ctx.JSON(data) } func ContabilidadListConsolidados(c *fiber.Ctx) error { anio, _ := strconv.Atoi(c.Query("anio", "0")) items, err := models.ListConsolidados(anio) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(items) } // ============================================================================= // ─── CRUD: Cuentas ────────────────────────────────────────────────────────── // ============================================================================= func GetCuentas(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) search := c.Query("search", "") if page < 1 { page = 1 } limit := 50 offset := (page - 1) * limit items, total, err := models.GetAllCuentas(limit, offset, search) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "items": items, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, }) } func GetCuentasSelect(c *fiber.Ctx) error { items, err := models.GetAllCuentasSelect() if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(items) } func CreateCuenta(c *fiber.Ctx) error { var req models.Cuenta if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } if req.Nombre == "" { return c.Status(400).JSON(fiber.Map{"error": "nombre es requerido"}) } if req.Tipo == "" { req.Tipo = "egreso" } req.Activo = true if err := models.CreateCuenta(&req); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.Status(201).JSON(req) } func UpdateCuenta(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 req models.Cuenta if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } req.ID = uint(id) if err := models.UpdateCuenta(&req); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } func DeleteCuenta(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.DeleteCuenta(uint(id)); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // ============================================================================= // ─── CRUD: Entidades ──────────────────────────────────────────────────────── // ============================================================================= func GetEntidades(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) search := c.Query("search", "") if page < 1 { page = 1 } limit := 20 offset := (page - 1) * limit items, total, err := models.GetAllEntidades(limit, offset, search) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "items": items, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, }) } func GetEntidadesSelect(c *fiber.Ctx) error { items, err := models.GetAllEntidadesSelect() if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(items) } func CreateEntidad(c *fiber.Ctx) error { var req models.Entidad if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } if req.Nombre == "" { return c.Status(400).JSON(fiber.Map{"error": "nombre es requerido"}) } req.Activo = true if err := models.CreateEntidad(&req); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.Status(201).JSON(req) } func UpdateEntidad(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 req models.Entidad if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } req.ID = uint(id) if err := models.UpdateEntidad(&req); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } func DeleteEntidad(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.DeleteEntidad(uint(id)); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // ============================================================================= // ─── CRUD: Transacciones ──────────────────────────────────────────────────── // ============================================================================= func GetTransacciones(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) search := c.Query("search", "") filtroTipo := c.Query("tipo", "") mes, _ := strconv.Atoi(c.Query("mes", "0")) anio, _ := strconv.Atoi(c.Query("anio", "0")) if page < 1 { page = 1 } limit := 30 offset := (page - 1) * limit items, total, err := models.GetAllTransacciones(limit, offset, search, filtroTipo, mes, anio) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "items": items, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, }) } func CreateTransaccion(c *fiber.Ctx) error { type Req struct { Fecha string `json:"fecha"` Tipo string `json:"tipo"` Descripcion string `json:"descripcion"` Valor float64 `json:"valor"` CuentaID *uint `json:"cuenta_id"` EntidadID *uint `json:"entidad_id"` FormaPago string `json:"forma_pago"` Estado string `json:"estado"` Notas string `json:"notas"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } if req.Descripcion == "" { return c.Status(400).JSON(fiber.Map{"error": "descripcion es requerida"}) } if req.Valor <= 0 { return c.Status(400).JSON(fiber.Map{"error": "valor debe ser mayor a 0"}) } t := &models.Transaccion{ Tipo: req.Tipo, Descripcion: req.Descripcion, Valor: req.Valor, CuentaID: req.CuentaID, EntidadID: req.EntidadID, FormaPago: req.FormaPago, Estado: req.Estado, Notas: req.Notas, Fecha: time.Now(), } if t.Tipo == "" { t.Tipo = "ingreso" } if t.Estado == "" { t.Estado = "registrada" } if req.Fecha != "" { if parsed, err := time.Parse("2006-01-02", req.Fecha); err == nil { t.Fecha = parsed } } if err := models.CreateTransaccion(t); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.Status(201).JSON(t) } func UpdateTransaccion(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 Req struct { Fecha string `json:"fecha"` Tipo string `json:"tipo"` Descripcion string `json:"descripcion"` Valor float64 `json:"valor"` CuentaID *uint `json:"cuenta_id"` EntidadID *uint `json:"entidad_id"` FormaPago string `json:"forma_pago"` Estado string `json:"estado"` Notas string `json:"notas"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } t := &models.Transaccion{ Tipo: req.Tipo, Descripcion: req.Descripcion, Valor: req.Valor, CuentaID: req.CuentaID, EntidadID: req.EntidadID, FormaPago: req.FormaPago, Estado: req.Estado, Notas: req.Notas, Fecha: time.Now(), } t.ID = uint(id) if req.Fecha != "" { if parsed, err := time.Parse("2006-01-02", req.Fecha); err == nil { t.Fecha = parsed } } if err := models.UpdateTransaccion(t); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } func DeleteTransaccion(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.DeleteTransaccion(uint(id)); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // ============================================================================= // ─── CRUD: Cuentas por Cobrar ────────────────────────────────────────────── // ============================================================================= func GetCuentasCobro(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) search := c.Query("search", "") estado := c.Query("estado", "") if page < 1 { page = 1 } limit := 20 offset := (page - 1) * limit items, total, err := models.GetAllCuentasCobro(limit, offset, search, estado) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "items": items, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, }) } 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"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } if req.ClienteID == 0 { return c.Status(400).JSON(fiber.Map{"error": "cliente_id es requerido"}) } cc := &models.CuentaCobro{ ClienteID: req.ClienteID, Descripcion: req.Descripcion, Valor: req.Valor, Estado: "pendiente", Notas: req.Notas, Fecha: time.Now(), } if req.Fecha != "" { if parsed, err := time.Parse("2006-01-02", req.Fecha); err == nil { cc.Fecha = parsed } } if req.FechaVencimiento != "" { if parsed, err := time.Parse("2006-01-02", req.FechaVencimiento); err == nil { cc.FechaVencimiento = &parsed } } if err := models.CreateCuentaCobro(cc); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.Status(201).JSON(cc) } func UpdateCuentaCobro(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 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"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } cc := &models.CuentaCobro{} cc.ID = uint(id) if req.Estado != "" { cc.Estado = req.Estado } if req.FechaPago != "" { if parsed, err := time.Parse("2006-01-02", req.FechaPago); err == nil { cc.FechaPago = &parsed } } cc.TransaccionID = req.TransaccionID cc.Notas = req.Notas if err := models.UpdateCuentaCobro(cc); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } func DeleteCuentaCobro(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.DeleteCuentaCobro(uint(id)); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // MarcarCuentaCobroPagada registra el pago y crea la transacción de ingreso vinculada. // POST /contabilidad/cuentas-cobro/:id/pagar func MarcarCuentaCobroPagada(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 req struct { FechaPago string `json:"fecha_pago"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } fecha := time.Now() if req.FechaPago != "" { if parsed, err := time.Parse("2006-01-02", req.FechaPago); err == nil { fecha = parsed } } if err := models.MarcarCuentaCobroPagada(uint(id), fecha); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // ============================================================================= // ─── CRUD: Cuentas por Pagar ──────────────────────────────────────────────── // ============================================================================= func GetCuentasPagar(c *fiber.Ctx) error { page, _ := strconv.Atoi(c.Query("page", "1")) search := c.Query("search", "") estado := c.Query("estado", "") if page < 1 { page = 1 } limit := 20 offset := (page - 1) * limit items, total, err := models.GetAllCuentasPagar(limit, offset, search, estado) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{ "items": items, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, }) } func CreateCuentaPagar(c *fiber.Ctx) error { type Req struct { EntidadID uint `json:"entidad_id"` Fecha string `json:"fecha"` Descripcion string `json:"descripcion"` Valor float64 `json:"valor"` Vencimiento string `json:"vencimiento"` Notas string `json:"notas"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } if req.EntidadID == 0 { return c.Status(400).JSON(fiber.Map{"error": "entidad_id es requerido"}) } cp := &models.CuentaPagar{ EntidadID: req.EntidadID, Descripcion: req.Descripcion, Valor: req.Valor, Estado: "pendiente", Notas: req.Notas, Fecha: time.Now(), } if req.Fecha != "" { if parsed, err := time.Parse("2006-01-02", req.Fecha); err == nil { cp.Fecha = parsed } } if req.Vencimiento != "" { if parsed, err := time.Parse("2006-01-02", req.Vencimiento); err == nil { cp.Vencimiento = &parsed } } if err := models.CreateCuentaPagar(cp); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.Status(201).JSON(cp) } func UpdateCuentaPagar(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 Req struct { 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 { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } cp := &models.CuentaPagar{} cp.ID = uint(id) if req.Estado != "" { cp.Estado = req.Estado } if req.FechaPago != "" { if parsed, err := time.Parse("2006-01-02", req.FechaPago); err == nil { cp.FechaPago = &parsed } } cp.TransaccionID = req.TransaccionID cp.Notas = req.Notas if err := models.UpdateCuentaPagar(cp); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } func MarcarCuentaPagarPagada(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 req struct { FechaPago string `json:"fecha_pago"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } fecha := time.Now() if req.FechaPago != "" { if parsed, err := time.Parse("2006-01-02", req.FechaPago); err == nil { fecha = parsed } } if err := models.MarcarCuentaPagarPagada(uint(id), fecha); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.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 { return c.Status(400).JSON(fiber.Map{"error": "ID inválido"}) } if err := models.DeleteCuentaPagar(uint(id)); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } // DownloadCuentaPagarArchivo sirve el soporte de una factura de compra // (subida desde el dashboard o adjuntada por Telegram). func DownloadCuentaPagarArchivo(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"}) } cp, err := models.GetCuentaPagarByID(uint(id)) if err != nil || cp.Archivo == "" { return c.Status(404).JSON(fiber.Map{"error": "Archivo no encontrado"}) } clean := filepath.Clean(cp.Archivo) if !strings.HasPrefix(clean, "uploads/") { return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"}) } nombre := cp.OriginalName if nombre == "" { nombre = filepath.Base(clean) } c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, nombre)) return c.SendFile(clean) }