package controllers import ( "fmt" "math" "os" "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" ) func FacturasIndex(c *fiber.Ctx) error { return c.Render("facturas", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main") } func LoadFacturas(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.GetAllFacturas(limit, offset, search) if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } clientes, _, _ := models.GetAllClientes(200, 0, "") proyectos, _, _ := models.GetAllProyectos(200, 0, "") return c.JSON(fiber.Map{ "items": items, "total": total, "totalPages": int(math.Ceil(float64(total) / float64(limit))), "page": page, "clientes": clientes, "proyectos": proyectos, }) } func CreateFactura(c *fiber.Ctx) error { type Req struct { ClienteID uint `json:"cliente_id"` ProyectoID *uint `json:"proyecto_id"` Numero string `json:"numero"` Descripcion string `json:"descripcion"` Monto float64 `json:"monto"` Moneda string `json:"moneda"` Estado string `json:"estado"` FechaEmision string `json:"fecha_emision"` FechaVencimiento string `json:"fecha_vencimiento"` Visible bool `json:"visible"` 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"}) } f := &models.Factura{ ClienteID: req.ClienteID, ProyectoID: req.ProyectoID, Numero: req.Numero, Descripcion: req.Descripcion, Monto: req.Monto, Moneda: req.Moneda, Estado: req.Estado, Visible: req.Visible, Notas: req.Notas, FechaEmision: time.Now(), } if req.FechaEmision != "" { t, err := time.Parse("2006-01-02", req.FechaEmision) if err == nil { f.FechaEmision = t } } if req.FechaVencimiento != "" { t, err := time.Parse("2006-01-02", req.FechaVencimiento) if err == nil { f.FechaVencimiento = &t } } if f.Moneda == "" { f.Moneda = "COP" } if f.Estado == "" { f.Estado = "pendiente" } if err := models.CreateFactura(f); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.Status(201).JSON(f) } func UpdateFactura(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 { ClienteID uint `json:"cliente_id"` ProyectoID *uint `json:"proyecto_id"` Numero string `json:"numero"` Descripcion string `json:"descripcion"` Monto float64 `json:"monto"` Moneda string `json:"moneda"` Estado string `json:"estado"` FechaEmision string `json:"fecha_emision"` FechaVencimiento string `json:"fecha_vencimiento"` Visible bool `json:"visible"` Notas string `json:"notas"` } var req Req if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } f := &models.Factura{ ClienteID: req.ClienteID, ProyectoID: req.ProyectoID, Numero: req.Numero, Descripcion: req.Descripcion, Monto: req.Monto, Moneda: req.Moneda, Estado: req.Estado, Visible: req.Visible, Notas: req.Notas, } f.ID = uint(id) if req.FechaEmision != "" { t, err := time.Parse("2006-01-02", req.FechaEmision) if err == nil { f.FechaEmision = t } } if req.FechaVencimiento != "" { t, err := time.Parse("2006-01-02", req.FechaVencimiento) if err == nil { f.FechaVencimiento = &t } } if err := models.UpdateFactura(f); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } func DeleteFactura(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.DeleteFactura(uint(id)); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } func UploadFacturaPDF(c *fiber.Ctx) error { id, _ := strconv.ParseUint(c.Params("id"), 10, 32) file, err := c.FormFile("pdf") if err != nil { return c.Status(400).JSON(fiber.Map{"error": "Archivo PDF requerido"}) } if file.Size > 20*1024*1024 { return c.Status(400).JSON(fiber.Map{"error": "Máximo 20MB"}) } dir := "uploads/facturas" if err := os.MkdirAll(dir, 0755); err != nil { return c.Status(500).JSON(fiber.Map{"error": "Error al crear directorio"}) } safeFile := safeFilenameProyecto(file.Filename) savePath := filepath.Join(dir, fmt.Sprintf("%d_%s", id, safeFile)) if err := c.SaveFile(file, savePath); err != nil { return c.Status(500).JSON(fiber.Map{"error": "Error al guardar archivo"}) } if err := models.UpdateFacturaArchivo(uint(id), savePath, file.Filename); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } // Notificar al cliente if f, err := models.GetFacturaByID(uint(id)); err == nil { go services.DispatchFacturaSubida(f) } return c.JSON(fiber.Map{"ok": true, "archivo": savePath}) } func DownloadFacturaPDF(c *fiber.Ctx) error { id, _ := strconv.ParseUint(c.Params("id"), 10, 32) f, err := models.GetFacturaByID(uint(id)) if err != nil || f.Archivo == "" { return c.Status(404).JSON(fiber.Map{"error": "Archivo no encontrado"}) } clean := filepath.Clean(f.Archivo) if !strings.HasPrefix(clean, "uploads/") { return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"}) } c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, f.OriginalName)) return c.SendFile(clean) }