package models import ( "encoding/json" "time" "github.com/sujit-baniya/fiber-boilerplate/app" "gorm.io/gorm" ) // ─── Proyecto ───────────────────────────────────────────────────────────────── type Proyecto struct { gorm.Model ClienteID uint `json:"cliente_id" gorm:"column:cliente_id;index;not null"` Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"` Nombre string `json:"nombre" gorm:"column:nombre;not null"` Slug string `json:"slug" gorm:"column:slug;uniqueIndex;not null"` Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"` Color string `json:"color" gorm:"column:color;default:'#8eb02f'"` Estado string `json:"estado" gorm:"column:estado;default:'activo'"` // activo|pausado|completado Progreso int `json:"progreso" gorm:"column:progreso;default:0"` // 0-100 } func (Proyecto) TableName() string { return "proyectos" } // ─── ProyectoFase (roadmap steps) ───────────────────────────────────────────── type ProyectoFase struct { gorm.Model ProyectoID uint `json:"proyecto_id" gorm:"column:proyecto_id;index"` Nombre string `json:"nombre" gorm:"column:nombre"` Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"` Estado string `json:"estado" gorm:"column:estado;default:'pendiente'"` // pendiente|en_progreso|completado Orden int `json:"orden" gorm:"column:orden;default:0"` FechaEstimada *time.Time `json:"fecha_estimada" gorm:"column:fecha_estimada"` FechaCompletado *time.Time `json:"fecha_completado" gorm:"column:fecha_completado"` EntregablesJSON string `json:"entregables_json" gorm:"column:entregables_json;type:text"` Entregables []string `json:"entregables" gorm:"-"` } func (ProyectoFase) TableName() string { return "proyecto_fases" } func (f *ProyectoFase) ParseEntregables() { if f.EntregablesJSON != "" { _ = json.Unmarshal([]byte(f.EntregablesJSON), &f.Entregables) } if f.Entregables == nil { f.Entregables = []string{} } } func (f *ProyectoFase) SerializeEntregables() { b, _ := json.Marshal(f.Entregables) f.EntregablesJSON = string(b) } // ─── ProyectoAvance (news feed / updates) ───────────────────────────────────── type ProyectoAvance struct { gorm.Model ProyectoID uint `json:"proyecto_id" gorm:"column:proyecto_id;index"` Titulo string `json:"titulo" gorm:"column:titulo"` Contenido string `json:"contenido" gorm:"column:contenido;type:text"` Tipo string `json:"tipo" gorm:"column:tipo;default:'update'"` // update|milestone|release Visible bool `json:"visible" gorm:"column:visible;default:true"` } func (ProyectoAvance) TableName() string { return "proyecto_avances" } // ─── ProyectoEntregable (downloadable files) ────────────────────────────────── type ProyectoEntregable struct { gorm.Model ProyectoID uint `json:"proyecto_id" gorm:"column:proyecto_id;index"` Nombre string `json:"nombre" gorm:"column:nombre"` Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"` Archivo string `json:"archivo" gorm:"column:archivo"` OriginalName string `json:"original_name" gorm:"column:original_name"` Version string `json:"version" gorm:"column:version"` TipoMime string `json:"tipo_mime" gorm:"column:tipo_mime"` Tamanio int64 `json:"tamanio" gorm:"column:tamanio"` Visible bool `json:"visible" gorm:"column:visible;default:true"` } func (ProyectoEntregable) TableName() string { return "proyecto_entregables" } // ─── Fase template (los 11 pasos de u-site) ─────────────────────────────────── var FaseTemplateDefault = []struct { Nombre string Descripcion string Entregables []string }{ {"Captación y calificación", "Marketing de contenidos, ads y referidos. Primer contacto y diagnóstico rápido. Identificación del tipo de cliente y evaluación de presupuesto.", []string{"Lead calificado", "Brief inicial del problema"}}, {"Descubrimiento y diagnóstico", "Reunión profunda sobre procesos y herramientas. Mapeo del flujo de trabajo actual. Identificación de cuellos de botella y priorización.", []string{"Documento de diagnóstico", "Lista de oportunidades"}}, {"Definición de solución y alcance", "Diseño de arquitectura modular. Definición de módulos, componentes e integraciones. Priorización por fases (MVP → evolución).", []string{"Propuesta técnica", "Alcance funcional", "Roadmap del proyecto"}}, {"Propuesta comercial y cierre", "Definición de modelo de cobro, presentación y ajustes según feedback. Negociación y firma.", []string{"Cotización formal", "Contrato / acuerdo", "Cronograma inicial"}}, {"Diseño funcional y técnico", "Wireframes o prototipos, definición de APIs e integraciones, diseño de base de datos.", []string{"Especificación funcional", "Arquitectura definida", "Backlog de desarrollo"}}, {"Desarrollo e implementación", "Desarrollo por módulos o iteraciones. Integraciones con herramientas externas. Reuniones de seguimiento.", []string{"Módulos funcionales", "Versiones iterativas"}}, {"Pruebas y validación", "Pruebas técnicas y con usuario (UAT). Ajustes, correcciones y validación de flujos completos.", []string{"Sistema validado", "Lista de ajustes finales"}}, {"Despliegue y puesta en producción", "Configuración de infraestructura, migración de datos, activación de automatizaciones y monitoreo inicial.", []string{"Sistema en producción", "Documentación básica de uso"}}, {"Capacitación y adopción", "Entrenamiento al equipo, guías rápidas o videos, resolución de dudas y ajustes por uso real.", []string{"Usuarios activos", "Procesos adoptados"}}, {"Soporte y mejora continua", "Soporte técnico, nuevas automatizaciones o módulos, optimización y evolución del sistema.", []string{"Roadmap evolutivo", "Mejoras continuas"}}, {"Cierre formal del proyecto", "Validación final, entrega de documentación, evaluación de resultados (antes vs después) y testimonio.", []string{"Acta de cierre", "Caso de estudio (opcional)", "Oportunidades de upsell"}}, } // ─── CRUD Proyecto ───────────────────────────────────────────────────────────── func GetAllProyectos(limit, offset int, search string) ([]Proyecto, int64, error) { var items []Proyecto var total int64 db := app.Http.Database.DB.Model(&Proyecto{}).Preload("Cliente") if search != "" { db = db.Where("proyectos.nombre ILIKE ? OR proyectos.descripcion ILIKE ?", "%"+search+"%", "%"+search+"%") } if err := db.Count(&total).Error; err != nil { return nil, 0, err } if err := db.Order("proyectos.created_at DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil { return nil, 0, err } return items, total, nil } func GetProyectosByClienteIDs(clienteIDs []uint) ([]Proyecto, error) { if len(clienteIDs) == 0 { return []Proyecto{}, nil } var items []Proyecto err := app.Http.Database.DB.Preload("Cliente").Where("cliente_id IN ?", clienteIDs).Order("created_at DESC").Find(&items).Error return items, err } func GetProyectoByID(id uint) (*Proyecto, error) { var item Proyecto err := app.Http.Database.DB.Preload("Cliente").First(&item, id).Error return &item, err } func GetProyectoBySlug(slug string) (*Proyecto, error) { var item Proyecto err := app.Http.Database.DB.Preload("Cliente").Where("slug = ?", slug).First(&item).Error return &item, err } func CreateProyecto(p *Proyecto) error { return app.Http.Database.DB.Create(p).Error } func UpdateProyecto(p *Proyecto) error { return app.Http.Database.DB.Model(&Proyecto{}).Where("id = ?", p.ID).Updates(map[string]interface{}{ "nombre": p.Nombre, "slug": p.Slug, "descripcion": p.Descripcion, "color": p.Color, "estado": p.Estado, "progreso": p.Progreso, "cliente_id": p.ClienteID, }).Error } func DeleteProyecto(id uint) error { return app.Http.Database.DB.Delete(&Proyecto{}, id).Error } // ─── CRUD ProyectoFase ───────────────────────────────────────────────────────── func GetFasesByProyecto(proyectoID uint) ([]ProyectoFase, error) { var items []ProyectoFase err := app.Http.Database.DB.Where("proyecto_id = ?", proyectoID).Order("orden ASC, id ASC").Find(&items).Error for i := range items { items[i].ParseEntregables() } return items, err } func CreateProyectoFase(f *ProyectoFase) error { f.SerializeEntregables() return app.Http.Database.DB.Create(f).Error } func UpdateProyectoFase(f *ProyectoFase) error { f.SerializeEntregables() return app.Http.Database.DB.Model(&ProyectoFase{}).Where("id = ?", f.ID).Updates(map[string]interface{}{ "nombre": f.Nombre, "descripcion": f.Descripcion, "estado": f.Estado, "orden": f.Orden, "fecha_estimada": f.FechaEstimada, "fecha_completado": f.FechaCompletado, "entregables_json": f.EntregablesJSON, }).Error } func DeleteProyectoFase(id uint) error { return app.Http.Database.DB.Delete(&ProyectoFase{}, id).Error } func ApplyFaseTemplate(proyectoID uint) error { if err := app.Http.Database.DB.Where("proyecto_id = ?", proyectoID).Delete(&ProyectoFase{}).Error; err != nil { return err } for i, tpl := range FaseTemplateDefault { ents, _ := json.Marshal(tpl.Entregables) fase := &ProyectoFase{ ProyectoID: proyectoID, Nombre: tpl.Nombre, Descripcion: tpl.Descripcion, Orden: i + 1, Estado: "pendiente", EntregablesJSON: string(ents), } if err := app.Http.Database.DB.Create(fase).Error; err != nil { return err } } return nil } // ─── CRUD ProyectoAvance ─────────────────────────────────────────────────────── func GetAvancesByProyecto(proyectoID uint, soloVisibles bool) ([]ProyectoAvance, error) { var items []ProyectoAvance db := app.Http.Database.DB.Where("proyecto_id = ?", proyectoID) if soloVisibles { db = db.Where("visible = true") } err := db.Order("created_at DESC").Find(&items).Error return items, err } func CreateProyectoAvance(a *ProyectoAvance) error { return app.Http.Database.DB.Create(a).Error } func UpdateProyectoAvance(a *ProyectoAvance) error { return app.Http.Database.DB.Model(&ProyectoAvance{}).Where("id = ?", a.ID).Updates(map[string]interface{}{ "titulo": a.Titulo, "contenido": a.Contenido, "tipo": a.Tipo, "visible": a.Visible, }).Error } func DeleteProyectoAvance(id uint) error { return app.Http.Database.DB.Delete(&ProyectoAvance{}, id).Error } // ─── CRUD ProyectoEntregable ─────────────────────────────────────────────────── func GetEntregablesByProyecto(proyectoID uint, soloVisibles bool) ([]ProyectoEntregable, error) { var items []ProyectoEntregable db := app.Http.Database.DB.Where("proyecto_id = ?", proyectoID) if soloVisibles { db = db.Where("visible = true") } err := db.Order("created_at DESC").Find(&items).Error return items, err } func CreateProyectoEntregable(e *ProyectoEntregable) error { return app.Http.Database.DB.Create(e).Error } func GetProyectoEntregableByID(id uint) (*ProyectoEntregable, error) { var item ProyectoEntregable err := app.Http.Database.DB.First(&item, id).Error return &item, err } func DeleteProyectoEntregable(id uint) error { return app.Http.Database.DB.Delete(&ProyectoEntregable{}, id).Error } func UpdateEntregableVisibilidad(id uint, visible bool) error { return app.Http.Database.DB.Model(&ProyectoEntregable{}).Where("id = ?", id).Update("visible", visible).Error } // ─── ProyectoDocumento (contratos y órdenes de servicio) ────────────────────── type ProyectoDocumento struct { gorm.Model ProyectoID uint `json:"proyecto_id" gorm:"column:proyecto_id;index"` Tipo string `json:"tipo" gorm:"column:tipo;default:'contrato'"` // contrato|orden_servicio|otro Nombre string `json:"nombre" gorm:"column:nombre"` Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"` Archivo string `json:"archivo" gorm:"column:archivo"` OriginalName string `json:"original_name" gorm:"column:original_name"` TipoMime string `json:"tipo_mime" gorm:"column:tipo_mime"` Tamanio int64 `json:"tamanio" gorm:"column:tamanio"` } func (ProyectoDocumento) TableName() string { return "proyecto_documentos" } func GetDocumentosByProyecto(proyectoID uint) ([]ProyectoDocumento, error) { var items []ProyectoDocumento err := app.Http.Database.DB.Where("proyecto_id = ?", proyectoID).Order("created_at DESC").Find(&items).Error return items, err } func CreateProyectoDocumento(d *ProyectoDocumento) error { return app.Http.Database.DB.Create(d).Error } func GetProyectoDocumentoByID(id uint) (*ProyectoDocumento, error) { var item ProyectoDocumento err := app.Http.Database.DB.First(&item, id).Error return &item, err } func DeleteProyectoDocumento(id uint) error { return app.Http.Database.DB.Delete(&ProyectoDocumento{}, id).Error }