330 lines
14 KiB
Go
330 lines
14 KiB
Go
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 CalcularProgresoDesdeFases(proyectoID uint) int {
|
|
var total, completados int64
|
|
app.Http.Database.DB.Model(&ProyectoFase{}).Where("proyecto_id = ?", proyectoID).Count(&total)
|
|
if total == 0 {
|
|
return 0
|
|
}
|
|
app.Http.Database.DB.Model(&ProyectoFase{}).Where("proyecto_id = ? AND estado = ?", proyectoID, "completado").Count(&completados)
|
|
return int((completados * 100) / total)
|
|
}
|
|
|
|
func ActualizarProgresoProyecto(proyectoID uint) error {
|
|
nuevo := CalcularProgresoDesdeFases(proyectoID)
|
|
return app.Http.Database.DB.Model(&Proyecto{}).Where("id = ?", proyectoID).Update("progreso", nuevo).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
|
|
}
|