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 }