package services import ( "encoding/json" "fmt" "os" "path/filepath" "time" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) // CrearTareaAsignada crea una tarea, opcionalmente ya asignada a alguien, y // dispara la notificación correspondiente (Telegram/email/sistema, según la // config de notif_evento_configs) si quedó asignada a un usuario. func CrearTareaAsignada(titulo, descripcion, estado, prioridad string, asignadoID *uint, fechaLimite *time.Time) (*models.Tarea, error) { if titulo == "" { return nil, fmt.Errorf("titulo requerido") } if estado == "" { // Debe coincidir con las columnas del Kanban (tareas.html): por_hacer, // en_progreso, revision, hecho. "pendiente" no es ninguna de esas — una // tarea con ese estado se guarda pero no aparece en ninguna columna. estado = "por_hacer" } if prioridad == "" { prioridad = "media" } t := &models.Tarea{ Titulo: titulo, Descripcion: descripcion, Estado: estado, Prioridad: prioridad, AsignadoID: asignadoID, FechaLimite: fechaLimite, } if err := models.CreateTarea(t); err != nil { return nil, fmt.Errorf("no se pudo crear la tarea: %w", err) } if asignadoID != nil { if full, err := models.GetTareaByID(t.ID); err == nil { go NotificarTareaAsignada(full) } } return t, nil } // AsignarTarea cambia el responsable de una tarea existente y notifica al nuevo // asignado si el responsable realmente cambió. asignadoID puede ser nil para // dejar la tarea sin asignar. func AsignarTarea(id uint, asignadoID *uint) (*models.Tarea, error) { if id == 0 { return nil, fmt.Errorf("id requerido") } tarea, err := models.GetTareaByID(id) if err != nil { return nil, fmt.Errorf("tarea no encontrada: %w", err) } prevAsignado := tarea.AsignadoID tarea.AsignadoID = asignadoID if err := models.SaveTarea(tarea); err != nil { return nil, fmt.Errorf("no se pudo asignar la tarea: %w", err) } full, _ := models.GetTareaByID(id) if cambioAsignado(prevAsignado, asignadoID) && full != nil { go NotificarTareaAsignada(full) } return full, nil } // AdjuntarDocumentoTarea guarda como comentario de una tarea el archivo (PDF/foto) // que el usuario acaba de enviar por Telegram en este chat. Reutiliza el mismo // formato de "archivos" (JSON) que ya usa el comentario web (AddComentario), para // que el archivo aparezca igual en /app/tareas sin importar de dónde se subió. func AdjuntarDocumentoTarea(chatID int64, tareaID uint, comentario string) (*models.TareaComentario, error) { if tareaID == 0 { return nil, fmt.Errorf("id de tarea requerido") } tarea, err := models.GetTareaByID(tareaID) if err != nil { return nil, fmt.Errorf("tarea no encontrada: %w", err) } att, ok := PopTelegramAttachment(chatID) if !ok { return nil, fmt.Errorf("no hay ningún documento pendiente en este chat; pide al usuario que lo envíe de nuevo") } if comentario == "" { comentario = fmt.Sprintf("Archivo adjunto vía Telegram: %s", att.OriginalName) } dir := fmt.Sprintf("uploads/tareas/%d", tareaID) if err := os.MkdirAll(dir, 0755); err != nil { _ = os.Remove(att.Path) return nil, fmt.Errorf("no se pudo crear el directorio: %w", err) } ext := filepath.Ext(att.OriginalName) finalPath := filepath.Join(dir, fmt.Sprintf("%d_telegram%s", time.Now().UnixMilli(), ext)) if err := os.Rename(att.Path, finalPath); err != nil { return nil, fmt.Errorf("no se pudo guardar el archivo: %w", err) } archivosJSON, _ := json.Marshal([]map[string]string{ {"nombre": att.OriginalName, "ruta": finalPath}, }) c := &models.TareaComentario{ TareaID: tareaID, Contenido: comentario, Archivos: string(archivosJSON), } if err := models.CreateComentario(c); err != nil { return nil, fmt.Errorf("no se pudo guardar el comentario: %w", err) } go NotificarTareaComentario(tarea, comentario) return c, nil } func cambioAsignado(a, b *uint) bool { if a == nil && b == nil { return false } if a == nil || b == nil { return true } return *a != *b }