package controllers import ( "fmt" "os" "path/filepath" "strconv" "strings" "time" "unicode" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) func TareasIndex(c *fiber.Ctx) error { return c.Render("tareas", fiber.Map{ "user": c.Locals("user"), "modules": c.Locals("userModules"), }, "layouts/main") } func GetTareas(c *fiber.Ctx) error { items, err := models.GetAllTareas() if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(items) } func GetUsuariosSistema(c *fiber.Ctx) error { users, _, err := models.AllUsersSistema(100, 0, "") if err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(users) } func CreateTarea(c *fiber.Ctx) error { var req struct { Titulo string `json:"titulo"` Descripcion string `json:"descripcion"` Prioridad string `json:"prioridad"` AsignadoID *uint `json:"asignado_id"` FechaLimite *string `json:"fecha_limite"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } if strings.TrimSpace(req.Titulo) == "" { return c.Status(400).JSON(fiber.Map{"error": "titulo requerido"}) } autorID := extraerUserID(c) tarea := &models.Tarea{ Titulo: req.Titulo, Descripcion: req.Descripcion, Estado: "por_hacer", Prioridad: prioPorDefecto(req.Prioridad), AsignadoID: req.AsignadoID, CreadoPorID: autorID, } if req.FechaLimite != nil && *req.FechaLimite != "" { if t, err := time.Parse("2006-01-02", *req.FechaLimite); err == nil { tarea.FechaLimite = &t } } if err := models.CreateTarea(tarea); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } // reload con preloads t, _ := models.GetTareaByID(tarea.ID) go services.NotificarTareaAsignada(t) return c.Status(201).JSON(t) } func UpdateTarea(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"}) } tarea, err := models.GetTareaByID(uint(id)) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "no encontrado"}) } var req struct { Titulo string `json:"titulo"` Descripcion string `json:"descripcion"` Prioridad string `json:"prioridad"` AsignadoID *uint `json:"asignado_id"` FechaLimite *string `json:"fecha_limite"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } prevAsignado := tarea.AsignadoID tarea.Titulo = req.Titulo tarea.Descripcion = req.Descripcion tarea.Prioridad = prioPorDefecto(req.Prioridad) tarea.AsignadoID = req.AsignadoID tarea.FechaLimite = nil if req.FechaLimite != nil && *req.FechaLimite != "" { if t, err := time.Parse("2006-01-02", *req.FechaLimite); err == nil { tarea.FechaLimite = &t } } if err := models.SaveTarea(tarea); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } t, _ := models.GetTareaByID(tarea.ID) // notificar si cambió el asignado if cambioPuntero(prevAsignado, req.AsignadoID) { go services.NotificarTareaAsignada(t) } return c.JSON(t) } func CambiarEstadoTarea(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 { Estado string `json:"estado"` } if err := c.BodyParser(&req); err != nil { return c.Status(400).JSON(fiber.Map{"error": err.Error()}) } estados := map[string]bool{"por_hacer": true, "en_progreso": true, "revision": true, "hecho": true} if !estados[req.Estado] { return c.Status(400).JSON(fiber.Map{"error": "estado inválido"}) } if err := models.CambiarEstadoTarea(uint(id), req.Estado); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } t, _ := models.GetTareaByID(uint(id)) go services.NotificarTareaEstado(t) return c.JSON(fiber.Map{"ok": true}) } func DeleteTareaHandler(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.DeleteTarea(uint(id)); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(fiber.Map{"ok": true}) } func GetTareaDetalle(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"}) } tarea, err := models.GetTareaByID(uint(id)) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "no encontrado"}) } comentarios, _ := models.GetComentariosByTarea(uint(id)) return c.JSON(fiber.Map{"tarea": tarea, "comentarios": comentarios}) } func AddComentario(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"}) } tarea, err := models.GetTareaByID(uint(id)) if err != nil { return c.Status(404).JSON(fiber.Map{"error": "tarea no encontrada"}) } contenido := strings.TrimSpace(c.FormValue("contenido")) file, fileErr := c.FormFile("archivo") tieneArchivo := fileErr == nil && file != nil if contenido == "" && !tieneArchivo { return c.Status(400).JSON(fiber.Map{"error": "contenido requerido"}) } autorID := extraerUserID(c) archivosJSON := "[]" // archivo adjunto opcional if tieneArchivo { if file.Size > 50*1024*1024 { return c.Status(400).JSON(fiber.Map{"error": "Máximo 50MB por archivo"}) } dir := fmt.Sprintf("uploads/tareas/%d", id) if err := os.MkdirAll(dir, 0755); err != nil { return c.Status(500).JSON(fiber.Map{"error": "Error creando directorio"}) } safeFile := safeTareaFilename(file.Filename) savePath := filepath.Join(dir, fmt.Sprintf("%d_%s", time.Now().UnixMilli(), safeFile)) clean := filepath.Clean(savePath) if !strings.HasPrefix(clean, "uploads/") { return c.Status(400).JSON(fiber.Map{"error": "Ruta inválida"}) } if err := c.SaveFile(file, savePath); err != nil { return c.Status(500).JSON(fiber.Map{"error": "Error guardando archivo"}) } archivosJSON = fmt.Sprintf(`[{"nombre":"%s","ruta":"%s"}]`, escapeJSON(file.Filename), escapeJSON(savePath)) } comentario := &models.TareaComentario{ TareaID: uint(id), AutorID: autorID, Contenido: contenido, Archivos: archivosJSON, } if err := models.CreateComentario(comentario); err != nil { return c.Status(500).JSON(fiber.Map{"error": err.Error()}) } go services.NotificarTareaComentario(tarea, contenido) return c.Status(201).JSON(comentario) } func extraerUserID(c *fiber.Ctx) uint { userMap, _ := c.Locals("user").(map[string]interface{}) if userMap == nil { return 0 } switch v := userMap["id"].(type) { case float64: return uint(v) case uint: return v } return 0 } func prioPorDefecto(p string) string { valid := map[string]bool{"baja": true, "media": true, "alta": true, "urgente": true} if valid[p] { return p } return "media" } func cambioPuntero(a, b *uint) bool { if a == nil && b == nil { return false } if a == nil || b == nil { return true } return *a != *b } func safeTareaFilename(name string) string { base := filepath.Base(name) var out []rune for _, r := range base { if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '.' || r == '-' || r == '_' { out = append(out, r) } else { out = append(out, '_') } } if len(out) == 0 { return "archivo" } return string(out) } func escapeJSON(s string) string { s = strings.ReplaceAll(s, `\`, `\\`) s = strings.ReplaceAll(s, `"`, `\"`) return s }