Files
soft_usite/rest/controllers/tarea_controller.go
T
Lizandro GuarnizoandClaude Sonnet 4.6 bc79553b7e feat(tareas): módulo de tablero kanban con drag & drop, comentarios y Telegram
- Modelos Tarea y TareaComentario con GORM, migración automática
- Estados: por_hacer → en_progreso → revisión → hecho; prioridades: baja/media/alta/urgente
- Drag & drop entre columnas via SortableJS (PUT /tarea/:id/estado)
- CRUD completo: crear, editar, eliminar; asignación a usuario + fecha límite
- Panel lateral de detalle: historial de comentarios, adjuntar archivos (uploads/tareas/:id)
- Telegram: notifica asignación, cambio de estado y comentarios nuevos via sendTelegramAdmin
- Seed automático bajo módulo "Administración", solo rol Administrador

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-06 20:18:26 -05:00

276 lines
7.5 KiB
Go

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"))
if contenido == "" {
return c.Status(400).JSON(fiber.Map{"error": "contenido requerido"})
}
autorID := extraerUserID(c)
archivosJSON := "[]"
// archivo adjunto opcional
file, fileErr := c.FormFile("archivo")
if fileErr == nil && file != nil {
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
}