This commit is contained in:
Lizandro Guarnizo
2026-05-14 22:52:10 -05:00
parent 61b3eca31d
commit 2377371b2c
14 changed files with 1938 additions and 1 deletions
+53
View File
@@ -0,0 +1,53 @@
package auth
import (
"errors"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/app"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
const portalSessionKey = "portal_user_id"
// PortalUser obtiene el usuario portal de la sesión actual.
func PortalUser(c *fiber.Ctx) (*models.PortalUser, error) {
store := app.Http.Session.Get(c)
raw := store.Get(portalSessionKey)
if raw == nil {
return nil, errors.New("no portal session")
}
id, ok := raw.(uint)
if !ok {
return nil, errors.New("invalid portal session")
}
u, err := models.GetPortalUserByID(id)
if err != nil {
return nil, err
}
if !u.Activo {
return nil, errors.New("portal user inactive")
}
// Precargar accesos del portal user para que GetClienteIDsForPortalUser funcione
return u, nil
}
// SetPortalSession guarda el portal_user_id en la sesión.
func SetPortalSession(c *fiber.Ctx, userID uint) error {
store := app.Http.Session.Get(c)
store.Set(portalSessionKey, userID)
return store.Save()
}
// DestroyPortalSession elimina la sesión del portal.
func DestroyPortalSession(c *fiber.Ctx) error {
store := app.Http.Session.Get(c)
store.Delete(portalSessionKey)
return store.Save()
}
// IsPortalLoggedIn verifica si hay sesión de portal activa.
func IsPortalLoggedIn(c *fiber.Ctx) bool {
store := app.Http.Session.Get(c)
return store.Get(portalSessionKey) != nil
}
+98
View File
@@ -0,0 +1,98 @@
package models
import (
"time"
"github.com/sujit-baniya/fiber-boilerplate/app"
"gorm.io/gorm"
)
// ─── Factura ──────────────────────────────────────────────────────────────────
type Factura struct {
gorm.Model
ClienteID uint `json:"cliente_id" gorm:"column:cliente_id;index;not null"`
Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
ProyectoID *uint `json:"proyecto_id" gorm:"column:proyecto_id;index"`
Proyecto *Proyecto `json:"proyecto" gorm:"foreignKey:ProyectoID"`
Numero string `json:"numero" gorm:"column:numero"`
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
Monto float64 `json:"monto" gorm:"column:monto"`
Moneda string `json:"moneda" gorm:"column:moneda;default:'COP'"`
Estado string `json:"estado" gorm:"column:estado;default:'pendiente'"` // pendiente|pagada|vencida|cancelada
FechaEmision time.Time `json:"fecha_emision" gorm:"column:fecha_emision"`
FechaVencimiento *time.Time `json:"fecha_vencimiento" gorm:"column:fecha_vencimiento"`
Archivo string `json:"archivo" gorm:"column:archivo"`
OriginalName string `json:"original_name" gorm:"column:original_name"`
Visible bool `json:"visible" gorm:"column:visible;default:true"`
Notas string `json:"notas" gorm:"column:notas;type:text"`
}
func (Factura) TableName() string { return "facturas" }
// ─── CRUD ─────────────────────────────────────────────────────────────────────
func GetAllFacturas(limit, offset int, search string) ([]Factura, int64, error) {
var items []Factura
var total int64
db := app.Http.Database.DB.Model(&Factura{}).Preload("Cliente").Preload("Proyecto")
if search != "" {
db = db.Joins("JOIN clientes ON clientes.id = facturas.cliente_id").
Where("facturas.numero ILIKE ? OR clientes.nombre ILIKE ? OR clientes.empresa ILIKE ?",
"%"+search+"%", "%"+search+"%", "%"+search+"%")
}
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := db.Order("facturas.fecha_emision DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
return nil, 0, err
}
return items, total, nil
}
func GetFacturasByCliente(clienteID uint, soloVisibles bool) ([]Factura, error) {
var items []Factura
db := app.Http.Database.DB.Where("cliente_id = ?", clienteID)
if soloVisibles {
db = db.Where("visible = true")
}
err := db.Preload("Proyecto").Order("fecha_emision DESC").Find(&items).Error
return items, err
}
func GetFacturaByID(id uint) (*Factura, error) {
var item Factura
err := app.Http.Database.DB.Preload("Cliente").Preload("Proyecto").First(&item, id).Error
return &item, err
}
func CreateFactura(f *Factura) error {
return app.Http.Database.DB.Create(f).Error
}
func UpdateFactura(f *Factura) error {
return app.Http.Database.DB.Model(&Factura{}).Where("id = ?", f.ID).Updates(map[string]interface{}{
"cliente_id": f.ClienteID,
"proyecto_id": f.ProyectoID,
"numero": f.Numero,
"descripcion": f.Descripcion,
"monto": f.Monto,
"moneda": f.Moneda,
"estado": f.Estado,
"fecha_emision": f.FechaEmision,
"fecha_vencimiento": f.FechaVencimiento,
"visible": f.Visible,
"notas": f.Notas,
}).Error
}
func DeleteFactura(id uint) error {
return app.Http.Database.DB.Delete(&Factura{}, id).Error
}
func UpdateFacturaArchivo(id uint, archivo, originalName string) error {
return app.Http.Database.DB.Model(&Factura{}).Where("id = ?", id).Updates(map[string]interface{}{
"archivo": archivo,
"original_name": originalName,
}).Error
}
+142
View File
@@ -0,0 +1,142 @@
package models
import (
"errors"
"github.com/sujit-baniya/fiber-boilerplate/app"
"gorm.io/gorm"
)
// ─── PortalUser ───────────────────────────────────────────────────────────────
// PortalUser representa un usuario del portal de clientes (separado del admin).
// - Rol "cliente": accede a los proyectos de su ClienteID.
// - Rol "partner": accede a los proyectos de todos sus ClienteIDs via PortalAccesos.
type PortalUser struct {
gorm.Model
Nombre string `json:"nombre" gorm:"column:nombre;not null"`
Email string `json:"email" gorm:"column:email;uniqueIndex;not null"`
Password string `json:"-" gorm:"column:password;type:text"`
ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"` // nil si es partner
Cliente *Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
Rol string `json:"rol" gorm:"column:rol;default:'cliente'"` // cliente|partner
Activo bool `json:"activo" gorm:"column:activo;default:true"`
Notas string `json:"notas" gorm:"column:notas;type:text"`
PortalAccesos []PortalAcceso `json:"portal_accesos" gorm:"foreignKey:PortalUserID"`
}
func (PortalUser) TableName() string { return "portal_users" }
// ─── PortalAcceso (partners → multiple clients) ───────────────────────────────
type PortalAcceso struct {
gorm.Model
PortalUserID uint `json:"portal_user_id" gorm:"column:portal_user_id;index"`
ClienteID uint `json:"cliente_id" gorm:"column:cliente_id;index"`
Cliente Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
}
func (PortalAcceso) TableName() string { return "portal_accesos" }
// ─── CRUD ─────────────────────────────────────────────────────────────────────
func GetAllPortalUsers(limit, offset int) ([]PortalUser, int64, error) {
var items []PortalUser
var total int64
db := app.Http.Database.DB.Model(&PortalUser{}).Preload("Cliente").Preload("PortalAccesos.Cliente")
if err := db.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := db.Order("created_at DESC").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
return nil, 0, err
}
return items, total, nil
}
func GetPortalUserByID(id uint) (*PortalUser, error) {
var item PortalUser
err := app.Http.Database.DB.Preload("Cliente").Preload("PortalAccesos.Cliente").First(&item, id).Error
return &item, err
}
func GetPortalUserByEmail(email string) (*PortalUser, error) {
var item PortalUser
err := app.Http.Database.DB.Where("email = ?", email).First(&item).Error
return &item, err
}
func CreatePortalUser(u *PortalUser) error {
return app.Http.Database.DB.Create(u).Error
}
func UpdatePortalUser(u *PortalUser) error {
return app.Http.Database.DB.Model(&PortalUser{}).Where("id = ?", u.ID).Updates(map[string]interface{}{
"nombre": u.Nombre,
"email": u.Email,
"cliente_id": u.ClienteID,
"rol": u.Rol,
"activo": u.Activo,
"notas": u.Notas,
}).Error
}
func UpdatePortalUserPassword(id uint, hashedPassword string) error {
return app.Http.Database.DB.Model(&PortalUser{}).Where("id = ?", id).Update("password", hashedPassword).Error
}
func DeletePortalUser(id uint) error {
// Eliminar también sus accesos de partner
if err := app.Http.Database.DB.Where("portal_user_id = ?", id).Delete(&PortalAcceso{}).Error; err != nil {
return err
}
return app.Http.Database.DB.Delete(&PortalUser{}, id).Error
}
func AddPortalAcceso(portalUserID, clienteID uint) error {
// Evitar duplicados
var existing PortalAcceso
err := app.Http.Database.DB.Where("portal_user_id = ? AND cliente_id = ?", portalUserID, clienteID).First(&existing).Error
if err == nil {
return nil // ya existe
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
return app.Http.Database.DB.Create(&PortalAcceso{PortalUserID: portalUserID, ClienteID: clienteID}).Error
}
func RemovePortalAcceso(portalUserID, clienteID uint) error {
return app.Http.Database.DB.Where("portal_user_id = ? AND cliente_id = ?", portalUserID, clienteID).
Delete(&PortalAcceso{}).Error
}
// GetClienteIDsForPortalUser devuelve todos los clienteIDs accesibles para un portal user.
func GetClienteIDsForPortalUser(u *PortalUser) []uint {
if u.Rol == "partner" {
ids := make([]uint, 0, len(u.PortalAccesos))
for _, a := range u.PortalAccesos {
ids = append(ids, a.ClienteID)
}
return ids
}
if u.ClienteID != nil {
return []uint{*u.ClienteID}
}
return []uint{}
}
// CheckPortalLogin valida credenciales y retorna el portal user.
func CheckPortalLogin(email, password string) (*PortalUser, error) {
u, err := GetPortalUserByEmail(email)
if err != nil {
return nil, errors.New("Usuario no encontrado o inactivo")
}
if !u.Activo {
return nil, errors.New("Usuario inactivo")
}
match, err := app.Http.Hash.Match(password, u.Password)
if err != nil || !match {
return nil, errors.New("Correo o contraseña incorrectos")
}
return u, nil
}
+275
View File
@@ -0,0 +1,275 @@
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) {
var items []Proyecto
err := app.Http.Database.DB.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
}
+69
View File
@@ -0,0 +1,69 @@
package models
import (
"github.com/sujit-baniya/fiber-boilerplate/app"
"gorm.io/gorm"
)
// ─── ProyectoTicket ───────────────────────────────────────────────────────────
type ProyectoTicket struct {
gorm.Model
ProyectoID uint `json:"proyecto_id" gorm:"column:proyecto_id;index"`
PortalUserID uint `json:"portal_user_id" gorm:"column:portal_user_id;index"`
AutorNombre string `json:"autor_nombre" gorm:"column:autor_nombre"`
Titulo string `json:"titulo" gorm:"column:titulo"`
Descripcion string `json:"descripcion" gorm:"column:descripcion;type:text"`
Estado string `json:"estado" gorm:"column:estado;default:'abierto'"` // abierto|en_progreso|resuelto|cerrado
Prioridad string `json:"prioridad" gorm:"column:prioridad;default:'media'"` // baja|media|alta|urgente
Mensajes []TicketMensaje `json:"mensajes" gorm:"foreignKey:TicketID"`
}
func (ProyectoTicket) TableName() string { return "proyecto_tickets" }
// ─── TicketMensaje ────────────────────────────────────────────────────────────
type TicketMensaje struct {
gorm.Model
TicketID uint `json:"ticket_id" gorm:"column:ticket_id;index"`
Contenido string `json:"contenido" gorm:"column:contenido;type:text"`
EsAdmin bool `json:"es_admin" gorm:"column:es_admin;default:false"`
AutorNombre string `json:"autor_nombre" gorm:"column:autor_nombre"`
}
func (TicketMensaje) TableName() string { return "ticket_mensajes" }
// ─── CRUD ─────────────────────────────────────────────────────────────────────
func GetTicketsByProyecto(proyectoID uint) ([]ProyectoTicket, error) {
var items []ProyectoTicket
err := app.Http.Database.DB.Where("proyecto_id = ?", proyectoID).
Preload("Mensajes").Order("created_at DESC").Find(&items).Error
return items, err
}
func GetTicketByID(id uint) (*ProyectoTicket, error) {
var item ProyectoTicket
err := app.Http.Database.DB.Preload("Mensajes").First(&item, id).Error
return &item, err
}
func CreateProyectoTicket(t *ProyectoTicket) error {
return app.Http.Database.DB.Create(t).Error
}
func UpdateTicketEstado(id uint, estado string) error {
return app.Http.Database.DB.Model(&ProyectoTicket{}).Where("id = ?", id).
Update("estado", estado).Error
}
func CreateTicketMensaje(m *TicketMensaje) error {
return app.Http.Database.DB.Create(m).Error
}
func GetTicketsByPortalUser(portalUserID uint) ([]ProyectoTicket, error) {
var items []ProyectoTicket
err := app.Http.Database.DB.Where("portal_user_id = ?", portalUserID).
Preload("Mensajes").Order("created_at DESC").Find(&items).Error
return items, err
}