up
This commit is contained in:
+30
-30
@@ -14,27 +14,27 @@ import (
|
|||||||
// - Rol "partner": accede a los proyectos de todos sus ClienteIDs via PortalAccesos.
|
// - Rol "partner": accede a los proyectos de todos sus ClienteIDs via PortalAccesos.
|
||||||
type PortalUser struct {
|
type PortalUser struct {
|
||||||
gorm.Model
|
gorm.Model
|
||||||
Nombre string `json:"nombre" gorm:"column:nombre;not null"`
|
Nombre string `json:"nombre" gorm:"column:nombre;not null"`
|
||||||
Email string `json:"email" gorm:"column:email;uniqueIndex;not null"`
|
Email string `json:"email" gorm:"column:email;uniqueIndex;not null"`
|
||||||
Password string `json:"-" gorm:"column:password;type:text"`
|
Password string `json:"-" gorm:"column:password;type:text"`
|
||||||
ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"` // nil si es partner
|
ClienteID *uint `json:"cliente_id" gorm:"column:cliente_id;index"` // nil si es partner
|
||||||
Cliente *Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
|
Cliente *Cliente `json:"cliente" gorm:"foreignKey:ClienteID"`
|
||||||
Rol string `json:"rol" gorm:"column:rol;default:'cliente'"` // cliente|partner
|
Rol string `json:"rol" gorm:"column:rol;default:'cliente'"` // cliente|partner
|
||||||
RoleID *uint `json:"role_id" gorm:"column:role_id;index"`
|
RoleID *uint `json:"role_id" gorm:"column:role_id;index"`
|
||||||
Role *Roles `json:"role" gorm:"foreignKey:RoleID"`
|
Role *Roles `json:"role" gorm:"foreignKey:RoleID"`
|
||||||
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||||
TelegramChatID string `json:"telegram_chat_id" gorm:"column:telegram_chat_id"`
|
TelegramChatID string `json:"telegram_chat_id" gorm:"column:telegram_chat_id"`
|
||||||
Telefono string `json:"telefono" gorm:"column:telefono"`
|
Telefono string `json:"telefono" gorm:"column:telefono"`
|
||||||
Indicativo string `json:"indicativo" gorm:"column:indicativo"` // ej: +57, +1
|
Indicativo string `json:"indicativo" gorm:"column:indicativo"` // ej: +57, +1
|
||||||
Pais string `json:"pais" gorm:"column:pais"`
|
Pais string `json:"pais" gorm:"column:pais"`
|
||||||
Ciudad string `json:"ciudad" gorm:"column:ciudad"`
|
Ciudad string `json:"ciudad" gorm:"column:ciudad"`
|
||||||
Documento string `json:"documento" gorm:"column:documento"` // RUT, NIT, CC, etc.
|
Documento string `json:"documento" gorm:"column:documento"` // RUT, NIT, CC, etc.
|
||||||
Empresa string `json:"empresa" gorm:"column:empresa"` // empresa que representa
|
Empresa string `json:"empresa" gorm:"column:empresa"` // empresa que representa
|
||||||
SitioWeb string `json:"sitio_web" gorm:"column:sitio_web"` // sitio web del partner/cliente
|
SitioWeb string `json:"sitio_web" gorm:"column:sitio_web"` // sitio web del partner/cliente
|
||||||
DocumentoRutFile string `json:"documento_rut_file" gorm:"column:documento_rut_file"` // ruta del archivo RUT
|
DocumentoRutFile string `json:"documento_rut_file" gorm:"column:documento_rut_file"` // ruta del archivo RUT
|
||||||
DocumentoRutNombre string `json:"documento_rut_nombre" gorm:"column:documento_rut_nombre"` // nombre original
|
DocumentoRutNombre string `json:"documento_rut_nombre" gorm:"column:documento_rut_nombre"` // nombre original
|
||||||
PortalAccesos []PortalAcceso `json:"portal_accesos" gorm:"foreignKey:PortalUserID"`
|
PortalAccesos []PortalAcceso `json:"portal_accesos" gorm:"foreignKey:PortalUserID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (PortalUser) TableName() string { return "portal_users" }
|
func (PortalUser) TableName() string { return "portal_users" }
|
||||||
@@ -87,17 +87,17 @@ func UpdatePortalUser(u *PortalUser) error {
|
|||||||
"email": u.Email,
|
"email": u.Email,
|
||||||
"cliente_id": u.ClienteID,
|
"cliente_id": u.ClienteID,
|
||||||
"rol": u.Rol,
|
"rol": u.Rol,
|
||||||
"role_id": u.RoleID,
|
"role_id": u.RoleID,
|
||||||
"activo": u.Activo,
|
"activo": u.Activo,
|
||||||
"notas": u.Notas,
|
"notas": u.Notas,
|
||||||
"telegram_chat_id": u.TelegramChatID,
|
"telegram_chat_id": u.TelegramChatID,
|
||||||
"telefono": u.Telefono,
|
"telefono": u.Telefono,
|
||||||
"indicativo": u.Indicativo,
|
"indicativo": u.Indicativo,
|
||||||
"pais": u.Pais,
|
"pais": u.Pais,
|
||||||
"ciudad": u.Ciudad,
|
"ciudad": u.Ciudad,
|
||||||
"documento": u.Documento,
|
"documento": u.Documento,
|
||||||
"empresa": u.Empresa,
|
"empresa": u.Empresa,
|
||||||
"sitio_web": u.SitioWeb,
|
"sitio_web": u.SitioWeb,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,10 +152,10 @@
|
|||||||
<div class="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-4">
|
<div class="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-4">
|
||||||
<!-- Icono por tipo -->
|
<!-- Icono por tipo -->
|
||||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
|
<div class="w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0"
|
||||||
:class="d.tipo==='contrato' ? 'bg-blue-50' : d.tipo==='orden_servicio' ? 'bg-amber-50' : 'bg-slate-100'">
|
:class="d.tipo==='contrato' ? 'bg-blue-50 text-blue-500' : d.tipo==='orden_servicio' ? 'bg-amber-50 text-amber-500' : 'bg-slate-100 text-slate-400'">
|
||||||
<svg x-show="d.tipo==='contrato'" class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||||
<svg x-show="d.tipo==='orden_servicio'" class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/></svg>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||||
<svg x-show="d.tipo!=='contrato' && d.tipo!=='orden_servicio'" class="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"/></svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
@@ -163,13 +163,14 @@
|
|||||||
<div class="flex items-center gap-2 mt-0.5">
|
<div class="flex items-center gap-2 mt-0.5">
|
||||||
<span class="text-xs px-2 py-0.5 rounded-full font-medium capitalize"
|
<span class="text-xs px-2 py-0.5 rounded-full font-medium capitalize"
|
||||||
:class="d.tipo==='contrato' ? 'bg-blue-100 text-blue-700' : d.tipo==='orden_servicio' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-500'"
|
:class="d.tipo==='contrato' ? 'bg-blue-100 text-blue-700' : d.tipo==='orden_servicio' ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-500'"
|
||||||
x-text="d.tipo?.replace('_',' ')"></span>
|
x-text="(d.tipo||'').replace('_',' ')"></span>
|
||||||
<span x-show="d.descripcion" class="text-xs text-slate-400 truncate" x-text="d.descripcion"></span>
|
<span x-show="d.original_name" class="text-xs text-slate-400 truncate" x-text="d.original_name"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<p x-show="d.descripcion" class="text-xs text-slate-500 mt-0.5 truncate" x-text="d.descripcion"></p>
|
||||||
</div>
|
</div>
|
||||||
<!-- Descarga -->
|
<!-- Descarga -->
|
||||||
<a :href="`/portal/documentos/${d.ID}/download`"
|
<a :href="`/portal/documentos/${d.ID}/download`"
|
||||||
class="flex-shrink-0 text-[#8eb02f] hover:text-[#6d8c24]" title="Descargar">
|
class="flex-shrink-0 w-8 h-8 rounded-lg flex items-center justify-center text-[#8eb02f] hover:bg-[#8eb02f]/10 transition-colors" title="Descargar">
|
||||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -180,14 +180,14 @@ func PortalGetProyectoData(c *fiber.Ctx) error {
|
|||||||
documentos, _ := models.GetDocumentosByProyecto(proy.ID)
|
documentos, _ := models.GetDocumentosByProyecto(proy.ID)
|
||||||
|
|
||||||
return c.JSON(fiber.Map{
|
return c.JSON(fiber.Map{
|
||||||
"proyecto": proy,
|
"proyecto": proy,
|
||||||
"fases": fases,
|
"fases": fases,
|
||||||
"avances": avances,
|
"avances": avances,
|
||||||
"entregables": entregables,
|
"entregables": entregables,
|
||||||
"documentos": documentos,
|
"documentos": documentos,
|
||||||
"tickets": tickets,
|
"tickets": tickets,
|
||||||
"facturas": facturas,
|
"facturas": facturas,
|
||||||
"facturasNoLeidas": facturasNoLeidas,
|
"facturasNoLeidas": facturasNoLeidas,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +252,9 @@ func PortalResponderTicket(c *fiber.Ctx) error {
|
|||||||
return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
|
return c.Status(401).JSON(fiber.Map{"error": "no autenticado"})
|
||||||
}
|
}
|
||||||
ticketID, _ := c.ParamsInt("id")
|
ticketID, _ := c.ParamsInt("id")
|
||||||
type Req struct{ Contenido string `json:"contenido"` }
|
type Req struct {
|
||||||
|
Contenido string `json:"contenido"`
|
||||||
|
}
|
||||||
var req Req
|
var req Req
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
|
||||||
@@ -382,7 +384,7 @@ func PortalDownloadDocumento(c *fiber.Ctx) error {
|
|||||||
if !hasAccess {
|
if !hasAccess {
|
||||||
return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"})
|
return c.Status(403).JSON(fiber.Map{"error": "Sin acceso"})
|
||||||
}
|
}
|
||||||
clean := doc.Archivo
|
clean := filepath.Clean(doc.Archivo)
|
||||||
if !strings.HasPrefix(clean, "uploads/") {
|
if !strings.HasPrefix(clean, "uploads/") {
|
||||||
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
|
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
|
||||||
}
|
}
|
||||||
@@ -390,7 +392,13 @@ func PortalDownloadDocumento(c *fiber.Ctx) error {
|
|||||||
if nombre == "" {
|
if nombre == "" {
|
||||||
nombre = doc.Nombre
|
nombre = doc.Nombre
|
||||||
}
|
}
|
||||||
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, nombre))
|
safe := strings.Map(func(r rune) rune {
|
||||||
|
if r > 127 || r == '"' || r == '\\' || r == '/' || r == '\n' || r == '\r' {
|
||||||
|
return '_'
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}, nombre)
|
||||||
|
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, safe, url.PathEscape(nombre)))
|
||||||
return c.SendFile(clean)
|
return c.SendFile(clean)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -792,4 +800,3 @@ func searchTokenInUpdates(botToken, token string) (int64, bool) {
|
|||||||
}
|
}
|
||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -491,10 +492,25 @@ func DownloadDocumento(c *fiber.Ctx) error {
|
|||||||
if !strings.HasPrefix(clean, "uploads/") {
|
if !strings.HasPrefix(clean, "uploads/") {
|
||||||
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
|
return c.Status(403).JSON(fiber.Map{"error": "Acceso denegado"})
|
||||||
}
|
}
|
||||||
c.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, item.OriginalName))
|
nombre := item.OriginalName
|
||||||
|
if nombre == "" {
|
||||||
|
nombre = item.Nombre
|
||||||
|
}
|
||||||
|
c.Set("Content-Disposition", attachmentDisposition(nombre))
|
||||||
return c.SendFile(clean)
|
return c.SendFile(clean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// attachmentDisposition genera un header Content-Disposition con fallback ASCII y encoding RFC 5987.
|
||||||
|
func attachmentDisposition(name string) string {
|
||||||
|
safe := strings.Map(func(r rune) rune {
|
||||||
|
if r > 127 || r == '"' || r == '\\' || r == '/' || r == '\n' || r == '\r' {
|
||||||
|
return '_'
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}, name)
|
||||||
|
return fmt.Sprintf(`attachment; filename="%s"; filename*=UTF-8''%s`, safe, url.PathEscape(name))
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Tickets (admin) ──────────────────────────────────────────────────────────
|
// ─── Tickets (admin) ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
func GetTickets(c *fiber.Ctx) error {
|
func GetTickets(c *fiber.Ctx) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user