Files
soft_usite/pkg/services/proyecto_service.go
T
Lizandro GDandClaude Sonnet 5 a0b2e9ae0a feat: registrar proyectos con fases, adjuntar documentos y asignar tareas por Telegram
- crear_fase_proyecto: agrega fases/etapas a un proyecto (se puede llamar
  varias veces seguidas para cargar todas las fases de un proyecto nuevo).
- adjuntar_documento_proyecto: guarda el archivo que el usuario acaba de
  enviar por Telegram como documento del proyecto (mismo mecanismo de
  adjuntos ya usado para facturas — se generalizó TelegramAttachment para
  servir a ambos casos).
- listar_usuarios + asignado_id en crear_tarea + asignar_tarea: permite
  asignar tareas a alguien del equipo por Telegram, disparando la misma
  notificación (Telegram/email/sistema) que ya dispara el dashboard.
- El prompt del sistema ahora indica cómo decidir entre adjuntar_factura y
  adjuntar_documento_proyecto según el contexto del archivo recibido.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-03 02:11:02 +00:00

133 lines
3.9 KiB
Go

package services
import (
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
func slugifyProyecto(s string) string {
s = strings.ToLower(s)
replacer := strings.NewReplacer(
" ", "-", "á", "a", "é", "e", "í", "i", "ó", "o", "ú", "u",
"ñ", "n", "ü", "u",
)
s = replacer.Replace(s)
var b strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
b.WriteRune(r)
}
}
return strings.Trim(b.String(), "-")
}
// CrearProyectoSimple crea un proyecto para un cliente a partir del flujo de
// automatización con IA ("nombre, stack, cliente asociado"). El stack se antepone
// a la descripción; la creación de repo/estructura inicial queda fuera de este
// alcance (requiere integración con un proveedor Git, no cubierta aún).
func CrearProyectoSimple(clienteID uint, nombre, stack, descripcion string) (*models.Proyecto, error) {
if clienteID == 0 {
return nil, fmt.Errorf("cliente_id requerido")
}
if strings.TrimSpace(nombre) == "" {
return nil, fmt.Errorf("nombre requerido")
}
if stack != "" {
descripcion = fmt.Sprintf("Stack: %s\n\n%s", stack, descripcion)
}
p := &models.Proyecto{
ClienteID: clienteID,
Nombre: nombre,
Slug: slugifyProyecto(nombre),
Descripcion: descripcion,
Color: "#8eb02f",
Estado: "activo",
}
if err := models.CreateProyecto(p); err != nil {
return nil, fmt.Errorf("no se pudo crear el proyecto: %w", err)
}
return p, nil
}
// CrearFaseProyecto agrega una fase/etapa a un proyecto ya existente, para el
// flujo de automatización con IA ("registra el proyecto y coloca las fases").
func CrearFaseProyecto(proyectoID uint, nombre, descripcion, estado string, orden int, fechaEstimada *time.Time) (*models.ProyectoFase, error) {
if proyectoID == 0 {
return nil, fmt.Errorf("proyecto_id requerido")
}
if strings.TrimSpace(nombre) == "" {
return nil, fmt.Errorf("nombre requerido")
}
if estado == "" {
estado = "pendiente"
}
f := &models.ProyectoFase{
ProyectoID: proyectoID,
Nombre: nombre,
Descripcion: descripcion,
Estado: estado,
Orden: orden,
FechaEstimada: fechaEstimada,
}
if err := models.CreateProyectoFase(f); err != nil {
return nil, fmt.Errorf("no se pudo crear la fase: %w", err)
}
_ = models.ActualizarProgresoProyecto(proyectoID)
return f, nil
}
// AdjuntarDocumentoProyecto toma el archivo pendiente de Telegram para este chat
// (descargado por el webhook) y lo guarda como documento del proyecto, igual que
// si se subiera desde el dashboard.
func AdjuntarDocumentoProyecto(chatID int64, proyectoID uint, tipo, nombre, descripcion string) (*models.ProyectoDocumento, error) {
if proyectoID == 0 {
return nil, fmt.Errorf("proyecto_id requerido")
}
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 tipo == "" {
tipo = "otro"
}
if nombre == "" {
nombre = att.OriginalName
}
dir := fmt.Sprintf("uploads/proyectos/%d/docs", proyectoID)
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("telegram_%d%s", time.Now().UnixNano(), ext))
if err := os.Rename(att.Path, finalPath); err != nil {
return nil, fmt.Errorf("no se pudo guardar el archivo: %w", err)
}
var tamanio int64
if info, err := os.Stat(finalPath); err == nil {
tamanio = info.Size()
}
d := &models.ProyectoDocumento{
ProyectoID: proyectoID,
Tipo: tipo,
Nombre: nombre,
Descripcion: descripcion,
Archivo: finalPath,
OriginalName: att.OriginalName,
TipoMime: att.MimeType,
Tamanio: tamanio,
}
if err := models.CreateProyectoDocumento(d); err != nil {
return nil, err
}
return d, nil
}