- Nueva config "whisper" reutilizando /app/ai-config (credenciales/endpoint
validables) y servicio de transcripción compatible con OpenAI/Groq.
- El webhook de Telegram ahora detecta notas de voz/audio, las transcribe
y procesa el texto resultante como si el usuario lo hubiera escrito.
- Fix: subir un archivo adjunto en un comentario de tarea sin escribir
texto era rechazado por el backend ("contenido requerido") aunque el
frontend sí lo permitía.
277 lines
7.6 KiB
Go
277 lines
7.6 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"))
|
|
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
|
|
}
|