diff --git a/migrations/migrate.go b/migrations/migrate.go
index decf63b..c91f1a5 100755
--- a/migrations/migrate.go
+++ b/migrations/migrate.go
@@ -67,6 +67,11 @@ func Migrate() {
// Integraciones SaaS (dispatcher de pagos)
&models.SaasApiConfig{},
&models.SaasDispatchLog{},
+ // Documentos de clientes
+ &models.ClienteDocumento{},
+ // Telegram
+ &models.TelegramConfig{},
+ &models.TelegramLog{},
); err != nil {
log.Fatalf("Error during main migration: %v", err)
}
diff --git a/pkg/models/cliente_documento.go b/pkg/models/cliente_documento.go
new file mode 100644
index 0000000..cda5b5f
--- /dev/null
+++ b/pkg/models/cliente_documento.go
@@ -0,0 +1,47 @@
+package models
+
+import (
+ "time"
+
+ "github.com/sujit-baniya/fiber-boilerplate/app"
+ "gorm.io/gorm"
+)
+
+// ClienteDocumento almacena los archivos adjuntos de un cliente.
+type ClienteDocumento struct {
+ gorm.Model
+ ClienteID uint `json:"cliente_id" gorm:"column:cliente_id;not null;index"`
+ Nombre string `json:"nombre" gorm:"column:nombre"` // nombre descriptivo del doc
+ Archivo string `json:"archivo" gorm:"column:archivo"` // ruta relativa en disco
+ OriginalName string `json:"original_name" gorm:"column:original_name"` // nombre original del archivo
+ TipoMime string `json:"tipo_mime" gorm:"column:tipo_mime"`
+ Tamanio int64 `json:"tamanio" gorm:"column:tamanio"` // bytes
+ FechaExpedicion *time.Time `json:"fecha_expedicion" gorm:"column:fecha_expedicion"` // opcional
+}
+
+func (ClienteDocumento) TableName() string { return "cliente_documentos" }
+
+func GetDocumentosByCliente(clienteID uint) ([]ClienteDocumento, error) {
+ var items []ClienteDocumento
+ err := app.Http.Database.DB.
+ Where("cliente_id = ?", clienteID).
+ Order("created_at DESC").
+ Find(&items).Error
+ return items, err
+}
+
+func CreateClienteDocumento(d *ClienteDocumento) error {
+ return app.Http.Database.DB.Create(d).Error
+}
+
+func GetClienteDocumentoByID(id uint) (*ClienteDocumento, error) {
+ var d ClienteDocumento
+ if err := app.Http.Database.DB.First(&d, id).Error; err != nil {
+ return nil, err
+ }
+ return &d, nil
+}
+
+func DeleteClienteDocumento(id uint) error {
+ return app.Http.Database.DB.Delete(&ClienteDocumento{}, id).Error
+}
diff --git a/pkg/models/contrato.go b/pkg/models/contrato.go
index 3216659..0e24d99 100644
--- a/pkg/models/contrato.go
+++ b/pkg/models/contrato.go
@@ -16,6 +16,7 @@ type Contrato struct {
FechaInicio time.Time `json:"fecha_inicio" gorm:"column:fecha_inicio"`
FechaVencimiento time.Time `json:"fecha_vencimiento" gorm:"column:fecha_vencimiento"`
PrecioAcordado float64 `json:"precio_acordado" gorm:"column:precio_acordado"`
+ Moneda string `json:"moneda" gorm:"column:moneda;default:'COP'"`
Estado string `json:"estado" gorm:"column:estado;default:'activo'"` // activo | vencido | cancelado | renovado
AutoRenovar bool `json:"auto_renovar" gorm:"column:auto_renovar;default:false"`
Notas string `json:"notas" gorm:"column:notas"`
@@ -160,6 +161,7 @@ func UpdateContrato(c Contrato, servicioIDs []uint) error {
"fecha_inicio": c.FechaInicio,
"fecha_vencimiento": c.FechaVencimiento,
"precio_acordado": c.PrecioAcordado,
+ "moneda": c.Moneda,
"estado": c.Estado,
"auto_renovar": c.AutoRenovar,
"notas": c.Notas,
diff --git a/pkg/models/saas_producto.go b/pkg/models/saas_producto.go
index ee740a3..4d3c798 100644
--- a/pkg/models/saas_producto.go
+++ b/pkg/models/saas_producto.go
@@ -17,6 +17,7 @@ type SaasProducto struct {
Servicio *Servicio `json:"servicio" gorm:"foreignKey:ServicioID"`
Activo bool `json:"activo" gorm:"column:activo;default:true"`
Orden int `json:"orden" gorm:"column:orden;default:0"`
+ HealthURL string `json:"health_url" gorm:"column:health_url"` // URL para health check (opcional)
}
func (SaasProducto) TableName() string { return "saas_productos" }
@@ -74,6 +75,7 @@ func UpdateSaasProducto(s *SaasProducto) error {
"servicio_id": s.ServicioID,
"activo": s.Activo,
"orden": s.Orden,
+ "health_url": s.HealthURL,
}).Error
}
diff --git a/pkg/models/telegram_config.go b/pkg/models/telegram_config.go
new file mode 100644
index 0000000..ac362cb
--- /dev/null
+++ b/pkg/models/telegram_config.go
@@ -0,0 +1,80 @@
+package models
+
+import (
+ "github.com/sujit-baniya/fiber-boilerplate/app"
+ "gorm.io/gorm"
+)
+
+// TelegramConfig almacena un bot configurado con su chat_id de destino.
+type TelegramConfig struct {
+ gorm.Model
+ Nombre string `json:"nombre" gorm:"column:nombre;not null"`
+ BotToken string `json:"bot_token" gorm:"column:bot_token;type:text;not null"`
+ ChatID string `json:"chat_id" gorm:"column:chat_id;not null"` // puede ser número o @canal
+ Activo bool `json:"activo" gorm:"column:activo;default:true"`
+ Notas string `json:"notas" gorm:"column:notas;type:text"`
+}
+
+func (TelegramConfig) TableName() string { return "telegram_configs" }
+
+// TelegramLog registra cada mensaje enviado.
+type TelegramLog struct {
+ gorm.Model
+ TelegramConfigID uint `json:"telegram_config_id" gorm:"column:telegram_config_id;index"`
+ TelegramConfig TelegramConfig `json:"telegram_config" gorm:"foreignKey:TelegramConfigID"`
+ Titulo string `json:"titulo" gorm:"column:titulo"`
+ Mensaje string `json:"mensaje" gorm:"column:mensaje;type:text"`
+ Estado string `json:"estado" gorm:"column:estado"` // ok | failed
+ ErrorMsg string `json:"error_msg" gorm:"column:error_msg;type:text"`
+}
+
+func (TelegramLog) TableName() string { return "telegram_logs" }
+
+// ─── Queries ──────────────────────────────────────────────────────────────────
+
+func GetAllTelegramConfigs() ([]TelegramConfig, error) {
+ var items []TelegramConfig
+ err := app.Http.Database.DB.Order("created_at DESC").Find(&items).Error
+ return items, err
+}
+
+func GetTelegramConfigByID(id uint) (*TelegramConfig, error) {
+ var item TelegramConfig
+ err := app.Http.Database.DB.First(&item, id).Error
+ return &item, err
+}
+
+func CreateTelegramConfig(c *TelegramConfig) error {
+ return app.Http.Database.DB.Create(c).Error
+}
+
+func UpdateTelegramConfig(c *TelegramConfig) error {
+ return app.Http.Database.DB.Model(&TelegramConfig{}).Where("id = ?", c.ID).Updates(map[string]interface{}{
+ "nombre": c.Nombre,
+ "bot_token": c.BotToken,
+ "chat_id": c.ChatID,
+ "activo": c.Activo,
+ "notas": c.Notas,
+ }).Error
+}
+
+func DeleteTelegramConfig(id uint) error {
+ return app.Http.Database.DB.Delete(&TelegramConfig{}, id).Error
+}
+
+func GetTelegramLogs(limit, offset int) ([]TelegramLog, int64, error) {
+ var items []TelegramLog
+ var total int64
+ db := app.Http.Database.DB.Model(&TelegramLog{}).Preload("TelegramConfig")
+ 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 CreateTelegramLog(l *TelegramLog) error {
+ return app.Http.Database.DB.Create(l).Error
+}
diff --git a/resources/views/renovaciones/clientes.html b/resources/views/renovaciones/clientes.html
index 82f2d24..a4ca402 100644
--- a/resources/views/renovaciones/clientes.html
+++ b/resources/views/renovaciones/clientes.html
@@ -25,7 +25,7 @@
@@ -40,10 +40,13 @@
@@ -129,6 +132,120 @@
+
+
+
+
+
+
+
+
+
+
+
+ Subir documentos
+
+
+
+
+
+
+
+
+
+
+ Haz clic o arrastra archivos aquí
+ PDF, Word, Excel, imágenes, ZIP — máx. 20 MB c/u
+
+
+
+
+
+
+
+
+
+
+
+ Documentos guardados
+
+
+
+ Cargando…
+
+
+ Sin documentos
+
+
+
+
+
+
+
+
+
+
+
+ ¿Eliminar documento?
+
+
+
+
+
+
+
+
{
form: { nombre:'', empresa:'', email:'', email_cc:'', telefono:'', documento:'', notas:'', activo:true },
toast: { show: false, msg: '', type: 'ok' },
+ // ── Documentos ──────────────────────────────────────────────────────
+ docsModal: false,
+ docsCliente: null,
+ documentos: [],
+ docsLoading: false,
+ docsUploading: false,
+ docsUpload: { fechaExpedicion: '', files: [] },
+ deleteDocModal: false,
+ selectedDoc: null,
+
async init() { await this.loadData(); },
async loadData() {
@@ -201,7 +328,96 @@ document.addEventListener('alpine:init', () => {
showToast(msg, type='ok') {
this.toast = { show: true, msg, type };
setTimeout(() => this.toast.show = false, 3000);
- }
+ },
+
+ // ── Documentos: métodos ─────────────────────────────────────────────
+ async openDocs(d) {
+ this.docsCliente = d;
+ this.docsModal = true;
+ this.docsUpload = { fechaExpedicion: '', files: [] };
+ await this.loadDocs();
+ },
+
+ async loadDocs() {
+ this.docsLoading = true;
+ try {
+ const { data } = await axios.get(`/app/api/clientes/${this.docsCliente.ID}/documentos`);
+ this.documentos = data || [];
+ } catch(e) {
+ this.showToast('Error cargando documentos', 'error');
+ }
+ this.docsLoading = false;
+ },
+
+ addFiles(fileList) {
+ for (const f of fileList) {
+ if (!this.docsUpload.files.find(x => x.name === f.name && x.size === f.size)) {
+ this.docsUpload.files.push(f);
+ }
+ }
+ },
+
+ handleDrop(event) {
+ this.addFiles(event.dataTransfer.files);
+ },
+
+ removeFile(i) {
+ this.docsUpload.files.splice(i, 1);
+ },
+
+ async uploadDocs() {
+ if (this.docsUpload.files.length === 0) return;
+ this.docsUploading = true;
+ try {
+ const fd = new FormData();
+ for (const f of this.docsUpload.files) {
+ fd.append('archivos', f);
+ }
+ if (this.docsUpload.fechaExpedicion) {
+ fd.append('fecha_expedicion', this.docsUpload.fechaExpedicion);
+ }
+ await axios.post(`/app/api/clientes/${this.docsCliente.ID}/documentos`, fd, {
+ headers: { 'Content-Type': 'multipart/form-data' }
+ });
+ this.showToast('Archivos subidos correctamente');
+ this.docsUpload.files = [];
+ this.docsUpload.fechaExpedicion = '';
+ this.$refs.fileInput && (this.$refs.fileInput.value = '');
+ await this.loadDocs();
+ } catch(e) {
+ this.showToast(e.response?.data?.error || 'Error al subir', 'error');
+ }
+ this.docsUploading = false;
+ },
+
+ confirmDeleteDoc(doc) {
+ this.selectedDoc = doc;
+ this.deleteDocModal = true;
+ },
+
+ async deleteDoc() {
+ try {
+ await axios.delete(`/app/api/clientes/${this.docsCliente.ID}/documentos/${this.selectedDoc.ID}`);
+ this.showToast('Documento eliminado');
+ this.deleteDocModal = false;
+ this.selectedDoc = null;
+ await this.loadDocs();
+ } catch(e) {
+ this.showToast(e.response?.data?.error || 'Error al eliminar', 'error');
+ }
+ },
+
+ formatSize(bytes) {
+ if (!bytes) return '0 B';
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB';
+ return (bytes/(1024*1024)).toFixed(1) + ' MB';
+ },
+
+ fmtDate(iso) {
+ if (!iso) return '';
+ return iso.substring(0, 10);
+ },
}));
});
diff --git a/resources/views/renovaciones/contratos.html b/resources/views/renovaciones/contratos.html
index 8857acc..56da402 100644
--- a/resources/views/renovaciones/contratos.html
+++ b/resources/views/renovaciones/contratos.html
@@ -55,7 +55,7 @@
:class="d.urgencia==='rojo' ? 'bg-red-100 text-red-700' : d.urgencia==='amarillo' ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700'"
x-text="d.dias_restantes <= 0 ? 'Vencido' : d.dias_restantes+' días'">
|
- |
+ |
Precio acordado
- Sugerido:
+ Sugerido:
+
+
+
+
|
+
+
+
+
+
+
+
+
+
+
+
+ |
|
+
|