Compare commits
10
Commits
1d152b6de0
...
8a9235631c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a9235631c | ||
|
|
704664afe7 | ||
|
|
df1eb75bdd | ||
|
|
2136c46f42 | ||
|
|
f3d9794096 | ||
|
|
e5c6dba787 | ||
|
|
ab4e3252b1 | ||
|
|
f94d01c089 | ||
|
|
24c4f6b26f | ||
|
|
39f56372c3 |
@@ -117,6 +117,7 @@ func main() {
|
|||||||
migrations.SeedShield()
|
migrations.SeedShield()
|
||||||
migrations.SeedPartnerRecursos()
|
migrations.SeedPartnerRecursos()
|
||||||
models.SeedContabilidad()
|
models.SeedContabilidad()
|
||||||
|
models.SeedBalanceData()
|
||||||
migrations.SeedContabilidadMenu()
|
migrations.SeedContabilidadMenu()
|
||||||
migrations.SeedWebSms()
|
migrations.SeedWebSms()
|
||||||
// Iniciar cron de vencimientos
|
// Iniciar cron de vencimientos
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
@@ -276,6 +278,196 @@ func GetAllCuentasCobro(limit, offset int, search string, estado string) ([]Cuen
|
|||||||
return items, total, nil
|
return items, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SeedBalanceData() {
|
||||||
|
db := app.Http.Database.DB
|
||||||
|
|
||||||
|
parseDate := func(s string) time.Time {
|
||||||
|
t, _ := time.Parse("2006-01-02 15:04:05", s)
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
getEntidad := func(nombre string) *uint {
|
||||||
|
var e Entidad
|
||||||
|
if err := db.Where("nombre = ?", nombre).First(&e).Error; err == nil {
|
||||||
|
return &e.ID
|
||||||
|
}
|
||||||
|
e = Entidad{Nombre: nombre, Tipo: "cliente", Activo: true}
|
||||||
|
db.Create(&e)
|
||||||
|
return &e.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
getCuenta := func(codigo string) *uint {
|
||||||
|
var c Cuenta
|
||||||
|
if err := db.Where("codigo = ?", codigo).First(&c).Error; err == nil {
|
||||||
|
return &c.ID
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── FACTURAS → Transacciones (ingresos) ────────────────────────────────
|
||||||
|
facturas := []struct{ factura float64; valor float64; fecha string }{
|
||||||
|
{54, 450000, "2026-01-01 00:00:00"},
|
||||||
|
{55, 9000000, "2026-01-01 00:00:00"},
|
||||||
|
{56, 119000, "2026-01-01 00:00:00"},
|
||||||
|
{57, 330000, "2026-01-01 00:00:00"},
|
||||||
|
{58, 1000000, "2026-01-01 00:00:00"},
|
||||||
|
{59, 1000000, "2026-01-01 00:00:00"},
|
||||||
|
{60, 900000, "2026-01-01 00:00:00"},
|
||||||
|
{0, 0, "2026-01-01 00:00:00"}, // ajuste -130000
|
||||||
|
{61, 580000, "2026-02-01 00:00:00"},
|
||||||
|
{62, 330000, "2026-02-01 00:00:00"},
|
||||||
|
{63, 1785000, "2026-02-01 00:00:00"},
|
||||||
|
{64, 120000, "2026-02-01 00:00:00"},
|
||||||
|
{65, 350000, "2026-02-01 00:00:00"},
|
||||||
|
{66, 550000, "2026-02-01 00:00:00"},
|
||||||
|
{67, 1300000, "2026-02-01 00:00:00"},
|
||||||
|
{68, 270000, "2026-02-01 00:00:00"},
|
||||||
|
{69, 32000, "2026-02-01 00:00:00"},
|
||||||
|
{70, 3153500, "2026-02-01 00:00:00"},
|
||||||
|
{71, 349000, "2026-03-01 00:00:00"},
|
||||||
|
{72, 0, "2026-03-01 00:00:00"},
|
||||||
|
{73, 1380000, "2026-03-01 00:00:00"},
|
||||||
|
{74, 380000, "2026-03-01 00:00:00"},
|
||||||
|
{75, 2650000, "2026-04-01 00:00:00"},
|
||||||
|
{76, 390000, "2026-04-01 00:00:00"},
|
||||||
|
{77, 2200000, "2026-04-01 00:00:00"},
|
||||||
|
{78, 4400000, "2026-04-01 00:00:00"},
|
||||||
|
{79, 142000, "2026-04-01 00:00:00"},
|
||||||
|
}
|
||||||
|
cuentaIng := getCuenta("ING-FAC")
|
||||||
|
docuxer := getEntidad("DOCUXER")
|
||||||
|
giaf := getEntidad("GIAF SAS")
|
||||||
|
|
||||||
|
for _, f := range facturas {
|
||||||
|
if f.factura == 0 && f.valor == 0 {
|
||||||
|
// ajuste negativo: nota crédito
|
||||||
|
var existing Transaccion
|
||||||
|
if db.Where("descripcion = ?", "Ajuste/NC ene 2026").First(&existing).Error != nil {
|
||||||
|
db.Create(&Transaccion{
|
||||||
|
Fecha: parseDate("2026-01-01 00:00:00"),
|
||||||
|
Tipo: "egreso", Descripcion: "Ajuste/NC ene 2026",
|
||||||
|
Valor: 130000, CuentaID: cuentaIng, Estado: "registrada",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
desc := fmt.Sprintf("Factura #%.0f", f.factura)
|
||||||
|
var existing Transaccion
|
||||||
|
if db.Where("descripcion = ? AND fecha = ?", desc, parseDate(f.fecha)).First(&existing).Error != nil {
|
||||||
|
eid := docuxer
|
||||||
|
if f.factura == 56 {
|
||||||
|
eid = giaf
|
||||||
|
}
|
||||||
|
db.Create(&Transaccion{
|
||||||
|
Fecha: parseDate(f.fecha), Tipo: "ingreso",
|
||||||
|
Descripcion: desc, Valor: f.valor,
|
||||||
|
CuentaID: cuentaIng, EntidadID: eid, Estado: "registrada",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── IVA → Transacciones (egresos) ──────────────────────────────────────
|
||||||
|
ivas := []struct{ valor float64; entidad string; desc string }{
|
||||||
|
{285000, "DOCUXER", "IVA Fact #63"},
|
||||||
|
{503500, "DOCUXER", "IVA Fact #70"},
|
||||||
|
{19000, "GIAF SAS", "IVA Fact #56"},
|
||||||
|
{503500, "DOCUXER", "IVA Fact #75"},
|
||||||
|
{418000, "DOCUXER", "IVA Fact #77"},
|
||||||
|
}
|
||||||
|
cuentaImp := getCuenta("EGR-IMPU")
|
||||||
|
for _, iv := range ivas {
|
||||||
|
var existing Transaccion
|
||||||
|
if db.Where("descripcion = ?", iv.desc).First(&existing).Error != nil {
|
||||||
|
eid := getEntidad(iv.entidad)
|
||||||
|
db.Create(&Transaccion{
|
||||||
|
Fecha: parseDate("2026-01-01 00:00:00"),
|
||||||
|
Tipo: "egreso", Descripcion: iv.desc,
|
||||||
|
Valor: iv.valor, CuentaID: cuentaImp,
|
||||||
|
EntidadID: eid, Estado: "registrada",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── RETENCION → Transacciones (egresos) ────────────────────────────────
|
||||||
|
rets := []struct{ valor float64; fecha string; estado string }{
|
||||||
|
{195000, "2026-03-01 00:00:00", "pagado"},
|
||||||
|
{257000, "2026-02-01 00:00:00", "pagado"},
|
||||||
|
{431000, "2026-01-01 00:00:00", "pendiente"},
|
||||||
|
}
|
||||||
|
for _, r := range rets {
|
||||||
|
desc := fmt.Sprintf("Retención %s", r.fecha[:7])
|
||||||
|
var existing Transaccion
|
||||||
|
if db.Where("descripcion = ?", desc).First(&existing).Error != nil {
|
||||||
|
db.Create(&Transaccion{
|
||||||
|
Fecha: parseDate(r.fecha),
|
||||||
|
Tipo: "egreso", Descripcion: desc,
|
||||||
|
Valor: r.valor, CuentaID: cuentaImp,
|
||||||
|
Estado: "registrada",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── CUENTAS DE COBRO ───────────────────────────────────────────────────
|
||||||
|
cobros := []struct{ entidad string; valor float64; fecha string }{
|
||||||
|
{"NATALIA", 2000000, "2026-01-01 00:00:00"},
|
||||||
|
{"FELIPE", 2000000, "2026-01-01 00:00:00"},
|
||||||
|
{"CONTADORA", 1780000, "2026-01-01 00:00:00"},
|
||||||
|
{"FELIPE", 2300000, "2026-02-01 00:00:00"},
|
||||||
|
{"NATALIA", 2000000, "2026-02-01 00:00:00"},
|
||||||
|
{"FELIPE", 2300000, "2026-03-01 00:00:00"},
|
||||||
|
{"NATALIA", 2000000, "2026-03-01 00:00:00"},
|
||||||
|
{"FELIPE", 520000, "2026-04-01 00:00:00"},
|
||||||
|
{"ANDREMER", 80000, "2026-04-01 00:00:00"},
|
||||||
|
{"NATALIA", 2000000, "2026-04-01 00:00:00"},
|
||||||
|
}
|
||||||
|
for _, cb := range cobros {
|
||||||
|
eid := getEntidad(cb.entidad)
|
||||||
|
desc := fmt.Sprintf("Cobro %s %s", cb.entidad, cb.fecha[:7])
|
||||||
|
var existing CuentaCobro
|
||||||
|
if db.Where("descripcion = ?", desc).First(&existing).Error != nil {
|
||||||
|
df := parseDate(cb.fecha)
|
||||||
|
db.Create(&CuentaCobro{
|
||||||
|
EntidadID: *eid, Fecha: df,
|
||||||
|
Descripcion: desc, Valor: cb.valor,
|
||||||
|
Estado: "pendiente",
|
||||||
|
FechaVencimiento: &df,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── FAC POR PAGAR ──────────────────────────────────────────────────────
|
||||||
|
var existingCp CuentaPagar
|
||||||
|
if db.Where("descripcion = ?", "TECZONE ene 2026").First(&existingCp).Error != nil {
|
||||||
|
eid := getEntidad("TECZONE")
|
||||||
|
f := parseDate("2026-01-01 00:00:00")
|
||||||
|
db.Create(&CuentaPagar{
|
||||||
|
EntidadID: *eid, Fecha: f,
|
||||||
|
Descripcion: "TECZONE ene 2026",
|
||||||
|
Valor: 2687849, Estado: "pendiente",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── CONSOLIDADO POR MES ───────────────────────────────────────────────
|
||||||
|
consols := []struct{ mes int; ing, egre, resul float64 }{
|
||||||
|
{1, 12669000, 8467849, 4201151},
|
||||||
|
{2, 8470500, 4300000, 4170500},
|
||||||
|
{3, 2109000, 4300000, -2191000},
|
||||||
|
{4, 9782000, 2600000, 7182000},
|
||||||
|
}
|
||||||
|
for _, cs := range consols {
|
||||||
|
var existing ConsolidadoMensual
|
||||||
|
if db.Where("anio = 2026 AND mes = ?", cs.mes).First(&existing).Error != nil {
|
||||||
|
db.Create(&ConsolidadoMensual{
|
||||||
|
Anio: 2026, Mes: cs.mes,
|
||||||
|
TotalIngresos: cs.ing, TotalEgresos: cs.egre,
|
||||||
|
Resultado: cs.resul,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[SEED] Balance data imported from BALANCE.numbers")
|
||||||
|
}
|
||||||
|
|
||||||
func CreateCuentaCobro(cc *CuentaCobro) error {
|
func CreateCuentaCobro(cc *CuentaCobro) error {
|
||||||
return app.Http.Database.DB.Create(cc).Error
|
return app.Http.Database.DB.Create(cc).Error
|
||||||
}
|
}
|
||||||
@@ -494,6 +686,8 @@ func SeedContabilidad() {
|
|||||||
{Nombre: "DOCUXER", Tipo: "cliente"},
|
{Nombre: "DOCUXER", Tipo: "cliente"},
|
||||||
{Nombre: "FELIPE", Tipo: "cliente"},
|
{Nombre: "FELIPE", Tipo: "cliente"},
|
||||||
{Nombre: "NATALIA", Tipo: "cliente"},
|
{Nombre: "NATALIA", Tipo: "cliente"},
|
||||||
|
{Nombre: "CONTADORA", Tipo: "cliente"},
|
||||||
|
{Nombre: "ANDREMER", Tipo: "cliente"},
|
||||||
}
|
}
|
||||||
for _, e := range entidades {
|
for _, e := range entidades {
|
||||||
var existing Entidad
|
var existing Entidad
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/app"
|
"github.com/sujit-baniya/fiber-boilerplate/app"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -12,6 +15,13 @@ type WebSmsConfig struct {
|
|||||||
Sender string `json:"sender" gorm:"column:sender;size:30"`
|
Sender string `json:"sender" gorm:"column:sender;size:30"`
|
||||||
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
Notas string `json:"notas" gorm:"column:notas;type:text"`
|
||||||
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
Activo bool `json:"activo" gorm:"column:activo;default:true"`
|
||||||
|
SecretKey string `json:"secret_key" gorm:"column:secret_key;size:64;uniqueIndex"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateWebSmsSecret() string {
|
||||||
|
b := make([]byte, 24)
|
||||||
|
rand.Read(b)
|
||||||
|
return "sms_" + hex.EncodeToString(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (WebSmsConfig) TableName() string { return "websms_config" }
|
func (WebSmsConfig) TableName() string { return "websms_config" }
|
||||||
@@ -30,14 +40,22 @@ func SaveWebSmsConfig(s WebSmsConfig) error {
|
|||||||
Update("activo", false)
|
Update("activo", false)
|
||||||
s.Activo = true
|
s.Activo = true
|
||||||
if s.ID > 0 {
|
if s.ID > 0 {
|
||||||
|
// preserve existing secret_key
|
||||||
|
var existing WebSmsConfig
|
||||||
|
app.Http.Database.DB.First(&existing, s.ID)
|
||||||
|
if existing.SecretKey == "" {
|
||||||
|
existing.SecretKey = generateWebSmsSecret()
|
||||||
|
}
|
||||||
return app.Http.Database.DB.Model(&s).Updates(map[string]interface{}{
|
return app.Http.Database.DB.Model(&s).Updates(map[string]interface{}{
|
||||||
"username": s.Username,
|
"username": s.Username,
|
||||||
"api_token": s.ApiToken,
|
"api_token": s.ApiToken,
|
||||||
"sender": s.Sender,
|
"sender": s.Sender,
|
||||||
"notas": s.Notas,
|
"notas": s.Notas,
|
||||||
"activo": true,
|
"activo": true,
|
||||||
|
"secret_key": existing.SecretKey,
|
||||||
}).Error
|
}).Error
|
||||||
}
|
}
|
||||||
|
s.SecretKey = generateWebSmsSecret()
|
||||||
return app.Http.Database.DB.Create(&s).Error
|
return app.Http.Database.DB.Create(&s).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const websmsAPIBase = "https://websms.labsmobile.com/SY0204/api"
|
const websmsAPIBase = "https://api.labsmobile.com/json/send"
|
||||||
|
|
||||||
type WebSmsRequest struct {
|
type WebSmsRequest struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
@@ -92,9 +92,14 @@ func SendWebSms(cfg *models.WebSmsConfig, para, mensaje string) (*WebSmsResponse
|
|||||||
return nil, fmt.Errorf("websms API status %d: %s", resp.StatusCode, string(respBody))
|
return nil, fmt.Errorf("websms API status %d: %s", resp.StatusCode, string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ct := resp.Header.Get("Content-Type")
|
||||||
var result WebSmsResponse
|
var result WebSmsResponse
|
||||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
return nil, fmt.Errorf("websms: unmarshal: %w", err)
|
return nil, fmt.Errorf("websms: respuesta no JSON (Content-Type: %s, cuerpo: %.200s)", ct, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Code != "0" {
|
||||||
|
return nil, fmt.Errorf("websms: %s (code %s)", result.Message, result.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &result, nil
|
return &result, nil
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<div x-data="contabilidadApp()" x-init="init()" class="p-6">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800">Contabilidad</h1>
|
||||||
|
<p class="text-sm text-slate-500 mt-1">Consolidado mensual de ingresos y egresos</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<select x-model="mes" @change="loadDashboard()" class="input-field">
|
||||||
|
<template x-for="m in 12" :key="m">
|
||||||
|
<option :value="m" x-text="meses[m-1]"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
<select x-model="anio" @change="loadDashboard()" class="input-field">
|
||||||
|
<option x-text="anio-2" :value="anio-2"></option>
|
||||||
|
<option x-text="anio-1" :value="anio-1"></option>
|
||||||
|
<option x-text="anio" :value="anio" selected></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tarjetas de resumen -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
<p class="text-xs uppercase tracking-wide text-slate-500 font-semibold mb-1">Ingresos</p>
|
||||||
|
<p class="text-2xl font-bold text-green-600" x-text="formatoCOP(data.total_ingresos)"></p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
<p class="text-xs uppercase tracking-wide text-slate-500 font-semibold mb-1">Egresos</p>
|
||||||
|
<p class="text-2xl font-bold text-red-600" x-text="formatoCOP(data.total_egresos)"></p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
<p class="text-xs uppercase tracking-wide text-slate-500 font-semibold mb-1">Resultado</p>
|
||||||
|
<p class="text-2xl font-bold" :class="data.resultado >= 0 ? 'text-green-600' : 'text-red-600'" x-text="formatoCOP(data.resultado)"></p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
<p class="text-xs uppercase tracking-wide text-slate-500 font-semibold mb-1">Transacciones</p>
|
||||||
|
<p class="text-2xl font-bold text-blue-600" x-text="data.cant_transacciones"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pendientes -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<p class="text-sm font-semibold text-slate-700">Por cobrar</p>
|
||||||
|
<a href="/app/contabilidad/cuentas-cobro" class="text-xs text-blue-500 hover:underline">Ver todas</a>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl font-bold text-amber-600" x-text="formatoCOP(data.pendientes_cobro)"></p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<p class="text-sm font-semibold text-slate-700">Por pagar</p>
|
||||||
|
<a href="/app/contabilidad/cuentas-pagar" class="text-xs text-blue-500 hover:underline">Ver todas</a>
|
||||||
|
</div>
|
||||||
|
<p class="text-xl font-bold text-red-600" x-text="formatoCOP(data.pendientes_pago)"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Últimas transacciones -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-5">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800">Últimas transacciones</h2>
|
||||||
|
<a href="/app/contabilidad/transacciones" class="text-sm text-blue-500 hover:underline">Ver todas</a>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wide">
|
||||||
|
<tr>
|
||||||
|
<th class="px-3 py-2 text-left">Fecha</th>
|
||||||
|
<th class="px-3 py-2 text-left">Tipo</th>
|
||||||
|
<th class="px-3 py-2 text-left">Descripción</th>
|
||||||
|
<th class="px-3 py-2 text-left">Categoría</th>
|
||||||
|
<th class="px-3 py-2 text-left">Entidad</th>
|
||||||
|
<th class="px-3 py-2 text-right">Valor</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
<template x-for="t in data.transacciones" :key="t.ID">
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="px-3 py-2 text-xs text-slate-500" x-text="formatoFecha(t.fecha)"></td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<span class="badge" :class="t.tipo==='ingreso'?'badge-green':'badge-red'" x-text="t.tipo"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2 text-slate-700" x-text="t.descripcion"></td>
|
||||||
|
<td class="px-3 py-2 text-slate-600" x-text="t.cuenta?.nombre||'-'"></td>
|
||||||
|
<td class="px-3 py-2 text-slate-600" x-text="t.entidad?.nombre||'-'"></td>
|
||||||
|
<td class="px-3 py-2 text-right font-semibold" :class="t.tipo==='ingreso'?'text-green-600':'text-red-600'" x-text="formatoCOP(t.valor)"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template x-if="!data.transacciones?.length">
|
||||||
|
<tr><td colspan="6" class="text-center py-6 text-slate-400">Sin transacciones este mes</td></tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function contabilidadApp() {
|
||||||
|
return {
|
||||||
|
data:{transacciones:[]},
|
||||||
|
mes:new Date().getMonth()+1,
|
||||||
|
anio:new Date().getFullYear(),
|
||||||
|
meses:['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'],
|
||||||
|
|
||||||
|
async init(){ await this.loadDashboard(); },
|
||||||
|
|
||||||
|
async loadDashboard(){
|
||||||
|
try {
|
||||||
|
const r=await axios.get(`/app/contabilidad/dashboard?mes=${this.mes}&anio=${this.anio}`);
|
||||||
|
this.data=r.data;
|
||||||
|
} catch(e){ console.error(e); }
|
||||||
|
},
|
||||||
|
|
||||||
|
formatoCOP(n){ if(n==null) return '$0'; return '$ '+Number(n).toLocaleString('es-CO',{minimumFractionDigits:0}); },
|
||||||
|
formatoFecha(d){ if(!d) return ''; return new Date(d).toLocaleDateString('es-CO',{day:'2-digit',month:'short',year:'numeric'}); },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<div x-data="cuentasApp()" x-init="init()" class="p-6">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800">Categorías contables</h1>
|
||||||
|
<p class="text-sm text-slate-500 mt-1">Plan de cuentas para clasificar transacciones</p>
|
||||||
|
</div>
|
||||||
|
<button @click="openCreate()" class="btn-primary flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||||
|
Nueva categoría
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<input x-model="search" @input.debounce.400ms="page=1;load()" type="text" placeholder="Buscar por nombre o código..." class="input-field w-full max-w-sm">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wide">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left">Código</th>
|
||||||
|
<th class="px-4 py-3 text-left">Nombre</th>
|
||||||
|
<th class="px-4 py-3 text-left">Tipo</th>
|
||||||
|
<th class="px-4 py-3 text-left">Color</th>
|
||||||
|
<th class="px-4 py-3 text-left">Activo</th>
|
||||||
|
<th class="px-4 py-3 text-left">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
<template x-if="loading">
|
||||||
|
<tr><td colspan="6" class="text-center py-10 text-slate-400">Cargando...</td></tr>
|
||||||
|
</template>
|
||||||
|
<template x-for="c in items" :key="c.ID">
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="px-4 py-3 font-mono text-xs text-slate-500" x-text="c.codigo||'-'"></td>
|
||||||
|
<td class="px-4 py-3 text-slate-700">
|
||||||
|
<span class="inline-block w-3 h-3 rounded-full mr-2" :style="'background:'+(c.color||'#ccc')"></span>
|
||||||
|
<span x-text="c.nombre"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="badge" :class="c.tipo==='ingreso'?'badge-green':'badge-red'" x-text="c.tipo"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 font-mono text-xs text-slate-500" x-text="c.color||'-'"></td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span x-show="c.activo" class="text-green-500">Activo</span>
|
||||||
|
<span x-show="!c.activo" class="text-red-400">Inactivo</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button @click="openEdit(c)" class="btn-icon text-yellow-500">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="confirmDelete(c)" class="btn-icon text-red-500">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4" x-text="editId ? 'Editar categoría' : 'Nueva categoría'"></h2>
|
||||||
|
<form @submit.prevent="save()">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">Código</label>
|
||||||
|
<input x-model="form.codigo" class="input-field w-full" placeholder="ING-FAC">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Tipo</label>
|
||||||
|
<select x-model="form.tipo" class="input-field w-full" required>
|
||||||
|
<option value="ingreso">Ingreso</option>
|
||||||
|
<option value="egreso">Egreso</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Nombre</label>
|
||||||
|
<input x-model="form.nombre" class="input-field w-full" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Color</label>
|
||||||
|
<input x-model="form.color" type="color" class="h-10 w-full rounded border border-slate-200 cursor-pointer">
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 pt-5">
|
||||||
|
<input type="checkbox" x-model="form.activo" id="ca" class="w-4 h-4">
|
||||||
|
<label for="ca" class="text-sm text-slate-600">Activo</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="error" x-text="error" class="text-red-500 text-sm mt-3"></p>
|
||||||
|
<div class="flex justify-end gap-3 mt-5">
|
||||||
|
<button type="button" @click="showModal=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button type="submit" :disabled="saving" class="btn-primary" x-text="saving?'Guardando...':'Guardar'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-2">¿Eliminar categoría?</h2>
|
||||||
|
<p class="text-slate-600 text-sm mb-5">Esta acción no se puede deshacer.</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function cuentasApp() {
|
||||||
|
return {
|
||||||
|
items:[], total:0, totalPages:1, page:1, search:'',
|
||||||
|
loading:false, saving:false, showModal:false, showDelete:false,
|
||||||
|
editId:null, deleteId:null, error:'',
|
||||||
|
form:{ codigo:'', nombre:'', tipo:'egreso', color:'#3b82f6', activo:true },
|
||||||
|
|
||||||
|
async init(){ await this.load(); },
|
||||||
|
|
||||||
|
async load(){
|
||||||
|
this.loading=true;
|
||||||
|
try {
|
||||||
|
const r=await axios.get(`/app/contabilidad/cuentas/list?page=${this.page}&search=${encodeURIComponent(this.search)}`);
|
||||||
|
this.items=r.data.items; this.total=r.data.total; this.totalPages=r.data.totalPages;
|
||||||
|
} finally{ this.loading=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
openCreate(){ this.editId=null; this.error=''; this.form={codigo:'',nombre:'',tipo:'egreso',color:'#3b82f6',activo:true}; this.showModal=true; },
|
||||||
|
openEdit(c){
|
||||||
|
this.editId=c.ID; this.error='';
|
||||||
|
this.form={codigo:c.codigo||'',nombre:c.nombre,tipo:c.tipo,color:c.color||'#3b82f6',activo:c.activo};
|
||||||
|
this.showModal=true;
|
||||||
|
},
|
||||||
|
async save(){
|
||||||
|
this.saving=true; this.error='';
|
||||||
|
try {
|
||||||
|
if(this.editId) await axios.put(`/app/contabilidad/cuentas/${this.editId}`, this.form);
|
||||||
|
else await axios.post('/app/contabilidad/cuentas', this.form);
|
||||||
|
this.showModal=false; await this.load();
|
||||||
|
} catch(e){ this.error=e.response?.data?.error||'Error al guardar'; }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
confirmDelete(c){ this.deleteId=c.ID; this.showDelete=true; },
|
||||||
|
async doDelete(){
|
||||||
|
this.saving=true;
|
||||||
|
try{ await axios.delete(`/app/contabilidad/cuentas/${this.deleteId}`); this.showDelete=false; await this.load(); }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
<div x-data="cobroApp()" x-init="init()" class="p-6">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800">Cuentas por cobrar</h1>
|
||||||
|
<p class="text-sm text-slate-500 mt-1">Facturas y montos pendientes de cobro</p>
|
||||||
|
</div>
|
||||||
|
<button @click="openCreate()" class="btn-primary flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||||
|
Nuevo cobro
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||||
|
<input x-model="search" @input.debounce.400ms="page=1;load()" type="text" placeholder="Buscar..." class="input-field w-full max-w-xs">
|
||||||
|
<select x-model="filtroEstado" @change="page=1;load()" class="input-field">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
<option value="pendiente">Pendiente</option>
|
||||||
|
<option value="pagado">Pagado</option>
|
||||||
|
<option value="parcial">Parcial</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wide">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left">Fecha</th>
|
||||||
|
<th class="px-4 py-3 text-left">Entidad</th>
|
||||||
|
<th class="px-4 py-3 text-left">Descripción</th>
|
||||||
|
<th class="px-4 py-3 text-right">Valor</th>
|
||||||
|
<th class="px-4 py-3 text-left">Vence</th>
|
||||||
|
<th class="px-4 py-3 text-left">Estado</th>
|
||||||
|
<th class="px-4 py-3 text-left">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
<template x-if="loading">
|
||||||
|
<tr><td colspan="7" class="text-center py-10 text-slate-400">Cargando...</td></tr>
|
||||||
|
</template>
|
||||||
|
<template x-for="c in items" :key="c.ID">
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="px-4 py-3 text-xs text-slate-500" x-text="formatDate(c.fecha)"></td>
|
||||||
|
<td class="px-4 py-3 text-slate-700 font-medium" x-text="c.entidad?.nombre||'-'"></td>
|
||||||
|
<td class="px-4 py-3 text-slate-600" x-text="c.descripcion"></td>
|
||||||
|
<td class="px-4 py-3 text-right font-semibold text-amber-600" x-text="formatoCOP(c.valor)"></td>
|
||||||
|
<td class="px-4 py-3 text-xs text-slate-500" x-text="formatDate(c.fecha_vencimiento)"></td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="badge" :class="{'badge-yellow':c.estado==='pendiente','badge-green':c.estado==='pagado','badge-blue':c.estado==='parcial'}" x-text="c.estado"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button @click="marcarPagado(c)" class="btn-icon text-green-500" title="Marcar pagado" x-show="c.estado!=='pagado'">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="confirmDelete(c)" class="btn-icon text-red-500" title="Eliminar">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center mt-4 text-sm text-slate-500">
|
||||||
|
<span>Total: <strong x-text="total"></strong></span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button @click="page--;load()" :disabled="page<=1" class="px-3 py-1 rounded border border-slate-200 disabled:opacity-40">Ant</button>
|
||||||
|
<span class="px-3 py-1" x-text="`${page} / ${totalPages||1}`"></span>
|
||||||
|
<button @click="page++;load()" :disabled="page>=totalPages" class="px-3 py-1 rounded border border-slate-200 disabled:opacity-40">Sig</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal crear -->
|
||||||
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Nuevo cobro pendiente</h2>
|
||||||
|
<form @submit.prevent="save()">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Entidad (quien debe)</label>
|
||||||
|
<select x-model.number="form.entidad_id" class="input-field w-full" required>
|
||||||
|
<option value="">Seleccionar...</option>
|
||||||
|
<template x-for="e in entidades" :key="e.ID">
|
||||||
|
<option :value="e.ID" x-text="e.nombre"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Descripción</label>
|
||||||
|
<input x-model="form.descripcion" class="input-field w-full" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Valor ($)</label>
|
||||||
|
<input x-model.number="form.valor" type="number" step="0.01" class="input-field w-full" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Fecha</label>
|
||||||
|
<input x-model="form.fecha" type="date" class="input-field w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Fecha vencimiento</label>
|
||||||
|
<input x-model="form.fecha_vencimiento" type="date" class="input-field w-full">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Notas</label>
|
||||||
|
<textarea x-model="form.notas" class="input-field w-full" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="error" x-text="error" class="text-red-500 text-sm mt-3"></p>
|
||||||
|
<div class="flex justify-end gap-3 mt-5">
|
||||||
|
<button type="button" @click="showModal=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button type="submit" :disabled="saving" class="btn-primary" x-text="saving?'Guardando...':'Guardar'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal pagado -->
|
||||||
|
<div x-show="showPagarModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showPagarModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Marcar como pagado</h2>
|
||||||
|
<form @submit.prevent="doPagar()">
|
||||||
|
<label class="label">Fecha de pago</label>
|
||||||
|
<input x-model="pagoFecha" type="date" class="input-field w-full mb-4" required>
|
||||||
|
<div class="flex justify-end gap-3 mt-5">
|
||||||
|
<button type="button" @click="showPagarModal=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button type="submit" :disabled="saving" class="btn-primary" x-text="saving?'Guardando...':'Confirmar'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-2">¿Eliminar cobro?</h2>
|
||||||
|
<p class="text-slate-600 text-sm mb-5">Esta acción no se puede deshacer.</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function cobroApp() {
|
||||||
|
return {
|
||||||
|
items:[], entidades:[], total:0, totalPages:1, page:1, search:'', filtroEstado:'',
|
||||||
|
loading:false, saving:false, showModal:false, showDelete:false, showPagarModal:false,
|
||||||
|
deleteId:null, pagarId:null, pagoFecha:'', error:'',
|
||||||
|
form:{ entidad_id:'', descripcion:'', valor:0, fecha:'', fecha_vencimiento:'', notas:'' },
|
||||||
|
|
||||||
|
async init(){ await this.load(); },
|
||||||
|
|
||||||
|
async load(){
|
||||||
|
this.loading=true;
|
||||||
|
try {
|
||||||
|
const params=new URLSearchParams({page:this.page,search:this.search});
|
||||||
|
if(this.filtroEstado) params.set('estado',this.filtroEstado);
|
||||||
|
const r=await axios.get('/app/contabilidad/cuentas-cobro/list?'+params.toString());
|
||||||
|
this.items=r.data.items; this.total=r.data.total; this.totalPages=r.data.totalPages;
|
||||||
|
const er=await axios.get('/app/contabilidad/entidades/select');
|
||||||
|
this.entidades=er.data;
|
||||||
|
} finally{ this.loading=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
openCreate(){ this.editId=null; this.error=''; this.form={entidad_id:'',descripcion:'',valor:0,fecha:'',fecha_vencimiento:'',notas:''}; this.showModal=true; },
|
||||||
|
async save(){
|
||||||
|
this.saving=true; this.error='';
|
||||||
|
try {
|
||||||
|
await axios.post('/app/contabilidad/cuentas-cobro', this.form);
|
||||||
|
this.showModal=false; await this.load();
|
||||||
|
} catch(e){ this.error=e.response?.data?.error||'Error al guardar'; }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
marcarPagado(c){ this.pagarId=c.ID; this.pagoFecha=new Date().toISOString().substring(0,10); this.showPagarModal=true; },
|
||||||
|
async doPagar(){
|
||||||
|
this.saving=true;
|
||||||
|
try {
|
||||||
|
await axios.put(`/app/contabilidad/cuentas-cobro/${this.pagarId}`, {estado:'pagado',fecha_pago:this.pagoFecha});
|
||||||
|
this.showPagarModal=false; await this.load();
|
||||||
|
} catch(e){ this.error=e.response?.data?.error||'Error'; }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmDelete(c){ this.deleteId=c.ID; this.showDelete=true; },
|
||||||
|
async doDelete(){
|
||||||
|
this.saving=true;
|
||||||
|
try{ await axios.delete(`/app/contabilidad/cuentas-cobro/${this.deleteId}`); this.showDelete=false; await this.load(); }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
formatoCOP(n){ if(n==null) return '$0'; return '$ '+Number(n).toLocaleString('es-CO',{minimumFractionDigits:0}); },
|
||||||
|
formatDate(d){ if(!d) return ''; return new Date(d).toLocaleDateString('es-CO',{day:'2-digit',month:'short',year:'numeric'}); },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
<div x-data="pagarApp()" x-init="init()" class="p-6">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800">Cuentas por pagar</h1>
|
||||||
|
<p class="text-sm text-slate-500 mt-1">Facturas y montos pendientes de pago</p>
|
||||||
|
</div>
|
||||||
|
<button @click="openCreate()" class="btn-primary flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||||
|
Nuevo pendiente
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||||
|
<input x-model="search" @input.debounce.400ms="page=1;load()" type="text" placeholder="Buscar..." class="input-field w-full max-w-xs">
|
||||||
|
<select x-model="filtroEstado" @change="page=1;load()" class="input-field">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
<option value="pendiente">Pendiente</option>
|
||||||
|
<option value="pagado">Pagado</option>
|
||||||
|
<option value="parcial">Parcial</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wide">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left">Fecha</th>
|
||||||
|
<th class="px-4 py-3 text-left">Entidad</th>
|
||||||
|
<th class="px-4 py-3 text-left">Descripción</th>
|
||||||
|
<th class="px-4 py-3 text-right">Valor</th>
|
||||||
|
<th class="px-4 py-3 text-left">Vence</th>
|
||||||
|
<th class="px-4 py-3 text-left">Estado</th>
|
||||||
|
<th class="px-4 py-3 text-left">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
<template x-if="loading">
|
||||||
|
<tr><td colspan="7" class="text-center py-10 text-slate-400">Cargando...</td></tr>
|
||||||
|
</template>
|
||||||
|
<template x-for="c in items" :key="c.ID">
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="px-4 py-3 text-xs text-slate-500" x-text="formatDate(c.fecha)"></td>
|
||||||
|
<td class="px-4 py-3 text-slate-700 font-medium" x-text="c.entidad?.nombre||'-'"></td>
|
||||||
|
<td class="px-4 py-3 text-slate-600" x-text="c.descripcion"></td>
|
||||||
|
<td class="px-4 py-3 text-right font-semibold text-red-600" x-text="formatoCOP(c.valor)"></td>
|
||||||
|
<td class="px-4 py-3 text-xs text-slate-500" x-text="formatDate(c.vencimiento)"></td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="badge" :class="{'badge-yellow':c.estado==='pendiente','badge-green':c.estado==='pagado','badge-blue':c.estado==='parcial'}" x-text="c.estado"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button @click="marcarPagado(c)" class="btn-icon text-green-500" title="Marcar pagado" x-show="c.estado!=='pagado'">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="confirmDelete(c)" class="btn-icon text-red-500" title="Eliminar">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center mt-4 text-sm text-slate-500">
|
||||||
|
<span>Total: <strong x-text="total"></strong></span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button @click="page--;load()" :disabled="page<=1" class="px-3 py-1 rounded border border-slate-200 disabled:opacity-40">Ant</button>
|
||||||
|
<span class="px-3 py-1" x-text="`${page} / ${totalPages||1}`"></span>
|
||||||
|
<button @click="page++;load()" :disabled="page>=totalPages" class="px-3 py-1 rounded border border-slate-200 disabled:opacity-40">Sig</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal crear -->
|
||||||
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Nuevo pendiente de pago</h2>
|
||||||
|
<form @submit.prevent="save()">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Entidad (a quién debemos)</label>
|
||||||
|
<select x-model.number="form.entidad_id" class="input-field w-full" required>
|
||||||
|
<option value="">Seleccionar...</option>
|
||||||
|
<template x-for="e in entidades" :key="e.ID">
|
||||||
|
<option :value="e.ID" x-text="e.nombre"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Descripción</label>
|
||||||
|
<input x-model="form.descripcion" class="input-field w-full" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Valor ($)</label>
|
||||||
|
<input x-model.number="form.valor" type="number" step="0.01" class="input-field w-full" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Fecha</label>
|
||||||
|
<input x-model="form.fecha" type="date" class="input-field w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Fecha vencimiento</label>
|
||||||
|
<input x-model="form.fecha_vencimiento" type="date" class="input-field w-full">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Notas</label>
|
||||||
|
<textarea x-model="form.notas" class="input-field w-full" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="error" x-text="error" class="text-red-500 text-sm mt-3"></p>
|
||||||
|
<div class="flex justify-end gap-3 mt-5">
|
||||||
|
<button type="button" @click="showModal=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button type="submit" :disabled="saving" class="btn-primary" x-text="saving?'Guardando...':'Guardar'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal pagado -->
|
||||||
|
<div x-show="showPagarModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showPagarModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Marcar como pagado</h2>
|
||||||
|
<form @submit.prevent="doPagar()">
|
||||||
|
<label class="label">Fecha de pago</label>
|
||||||
|
<input x-model="pagoFecha" type="date" class="input-field w-full mb-4" required>
|
||||||
|
<div class="flex justify-end gap-3 mt-5">
|
||||||
|
<button type="button" @click="showPagarModal=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button type="submit" :disabled="saving" class="btn-primary" x-text="saving?'Guardando...':'Confirmar'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-2">¿Eliminar pendiente?</h2>
|
||||||
|
<p class="text-slate-600 text-sm mb-5">Esta acción no se puede deshacer.</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function pagarApp() {
|
||||||
|
return {
|
||||||
|
items:[], entidades:[], total:0, totalPages:1, page:1, search:'', filtroEstado:'',
|
||||||
|
loading:false, saving:false, showModal:false, showDelete:false, showPagarModal:false,
|
||||||
|
deleteId:null, pagarId:null, pagoFecha:'', error:'',
|
||||||
|
form:{ entidad_id:'', descripcion:'', valor:0, fecha:'', fecha_vencimiento:'', notas:'' },
|
||||||
|
|
||||||
|
async init(){ await this.load(); },
|
||||||
|
|
||||||
|
async load(){
|
||||||
|
this.loading=true;
|
||||||
|
try {
|
||||||
|
const params=new URLSearchParams({page:this.page,search:this.search});
|
||||||
|
if(this.filtroEstado) params.set('estado',this.filtroEstado);
|
||||||
|
const r=await axios.get('/app/contabilidad/cuentas-pagar/list?'+params.toString());
|
||||||
|
this.items=r.data.items; this.total=r.data.total; this.totalPages=r.data.totalPages;
|
||||||
|
const er=await axios.get('/app/contabilidad/entidades/select');
|
||||||
|
this.entidades=er.data;
|
||||||
|
} finally{ this.loading=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
openCreate(){ this.editId=null; this.error=''; this.form={entidad_id:'',descripcion:'',valor:0,fecha:'',fecha_vencimiento:'',notas:''}; this.showModal=true; },
|
||||||
|
async save(){
|
||||||
|
this.saving=true; this.error='';
|
||||||
|
try {
|
||||||
|
await axios.post('/app/contabilidad/cuentas-pagar', this.form);
|
||||||
|
this.showModal=false; await this.load();
|
||||||
|
} catch(e){ this.error=e.response?.data?.error||'Error al guardar'; }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
marcarPagado(c){ this.pagarId=c.ID; this.pagoFecha=new Date().toISOString().substring(0,10); this.showPagarModal=true; },
|
||||||
|
async doPagar(){
|
||||||
|
this.saving=true;
|
||||||
|
try {
|
||||||
|
await axios.put(`/app/contabilidad/cuentas-pagar/${this.pagarId}`, {estado:'pagado',fecha_pago:this.pagoFecha});
|
||||||
|
this.showPagarModal=false; await this.load();
|
||||||
|
} catch(e){ this.error=e.response?.data?.error||'Error'; }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmDelete(c){ this.deleteId=c.ID; this.showDelete=true; },
|
||||||
|
async doDelete(){
|
||||||
|
this.saving=true;
|
||||||
|
try{ await axios.delete(`/app/contabilidad/cuentas-pagar/${this.deleteId}`); this.showDelete=false; await this.load(); }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
formatoCOP(n){ if(n==null) return '$0'; return '$ '+Number(n).toLocaleString('es-CO',{minimumFractionDigits:0}); },
|
||||||
|
formatDate(d){ if(!d) return ''; return new Date(d).toLocaleDateString('es-CO',{day:'2-digit',month:'short',year:'numeric'}); },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<div x-data="entidadesApp()" x-init="init()" class="p-6">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800">Entidades</h1>
|
||||||
|
<p class="text-sm text-slate-500 mt-1">Clientes, proveedores y empresas</p>
|
||||||
|
</div>
|
||||||
|
<button @click="openCreate()" class="btn-primary flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||||
|
Nueva entidad
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<input x-model="search" @input.debounce.400ms="page=1;load()" type="text" placeholder="Buscar por nombre, documento..." class="input-field w-full max-w-sm">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wide">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left">Nombre</th>
|
||||||
|
<th class="px-4 py-3 text-left">Tipo</th>
|
||||||
|
<th class="px-4 py-3 text-left">Documento</th>
|
||||||
|
<th class="px-4 py-3 text-left">Email</th>
|
||||||
|
<th class="px-4 py-3 text-left">Teléfono</th>
|
||||||
|
<th class="px-4 py-3 text-left">Contacto</th>
|
||||||
|
<th class="px-4 py-3 text-left">Activo</th>
|
||||||
|
<th class="px-4 py-3 text-left">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
<template x-if="loading">
|
||||||
|
<tr><td colspan="8" class="text-center py-10 text-slate-400">Cargando...</td></tr>
|
||||||
|
</template>
|
||||||
|
<template x-for="e in items" :key="e.ID">
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="px-4 py-3 text-slate-700 font-medium" x-text="e.nombre"></td>
|
||||||
|
<td class="px-4 py-3"><span class="badge" :class="{'badge-green':e.tipo==='cliente','badge-blue':e.tipo==='proveedor','badge-slate':e.tipo==='ambos'}" x-text="e.tipo||'-'"></span></td>
|
||||||
|
<td class="px-4 py-3 text-slate-600 text-xs font-mono" x-text="e.documento||'-'"></td>
|
||||||
|
<td class="px-4 py-3 text-slate-600" x-text="e.email||'-'"></td>
|
||||||
|
<td class="px-4 py-3 text-slate-600" x-text="e.telefono||'-'"></td>
|
||||||
|
<td class="px-4 py-3 text-slate-600" x-text="e.contacto||'-'"></td>
|
||||||
|
<td class="px-4 py-3"><span x-show="e.activo" class="text-green-500">Activo</span><span x-show="!e.activo" class="text-red-400">Inactivo</span></td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button @click="openEdit(e)" class="btn-icon text-yellow-500" title="Editar">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="confirmDelete(e)" class="btn-icon text-red-500" title="Eliminar">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center mt-4 text-sm text-slate-500">
|
||||||
|
<span>Total: <strong x-text="total"></strong></span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button @click="page--;load()" :disabled="page<=1" class="px-3 py-1 rounded border border-slate-200 disabled:opacity-40">Ant</button>
|
||||||
|
<span class="px-3 py-1" x-text="`${page} / ${totalPages||1}`"></span>
|
||||||
|
<button @click="page++;load()" :disabled="page>=totalPages" class="px-3 py-1 rounded border border-slate-200 disabled:opacity-40">Sig</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4" x-text="editId?'Editar entidad':'Nueva entidad'"></h2>
|
||||||
|
<form @submit.prevent="save()">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Nombre</label>
|
||||||
|
<input x-model="form.nombre" class="input-field w-full" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Tipo</label>
|
||||||
|
<select x-model="form.tipo" class="input-field w-full">
|
||||||
|
<option value="">Seleccionar...</option>
|
||||||
|
<option value="cliente">Cliente</option>
|
||||||
|
<option value="proveedor">Proveedor</option>
|
||||||
|
<option value="ambos">Ambos</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Documento/NIT</label>
|
||||||
|
<input x-model="form.documento" class="input-field w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Email</label>
|
||||||
|
<input x-model="form.email" type="email" class="input-field w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Teléfono</label>
|
||||||
|
<input x-model="form.telefono" class="input-field w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Contacto persona</label>
|
||||||
|
<input x-model="form.contacto" class="input-field w-full">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Notas</label>
|
||||||
|
<textarea x-model="form.notas" class="input-field w-full" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2 flex items-center gap-2">
|
||||||
|
<input type="checkbox" x-model="form.activo" id="ea" class="w-4 h-4">
|
||||||
|
<label for="ea" class="text-sm text-slate-600">Activo</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="error" x-text="error" class="text-red-500 text-sm mt-3"></p>
|
||||||
|
<div class="flex justify-end gap-3 mt-5">
|
||||||
|
<button type="button" @click="showModal=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button type="submit" :disabled="saving" class="btn-primary" x-text="saving?'Guardando...':'Guardar'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-2">¿Eliminar entidad?</h2>
|
||||||
|
<p class="text-slate-600 text-sm mb-5">Esta acción no se puede deshacer.</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function entidadesApp() {
|
||||||
|
return {
|
||||||
|
items:[], total:0, totalPages:1, page:1, search:'',
|
||||||
|
loading:false, saving:false, showModal:false, showDelete:false,
|
||||||
|
editId:null, deleteId:null, error:'',
|
||||||
|
form:{ nombre:'', tipo:'', documento:'', email:'', telefono:'', contacto:'', notas:'', activo:true },
|
||||||
|
|
||||||
|
async init(){ await this.load(); },
|
||||||
|
|
||||||
|
async load(){
|
||||||
|
this.loading=true;
|
||||||
|
try {
|
||||||
|
const r=await axios.get(`/app/contabilidad/entidades?page=${this.page}&search=${encodeURIComponent(this.search)}`);
|
||||||
|
this.items=r.data.items; this.total=r.data.total; this.totalPages=r.data.totalPages;
|
||||||
|
} finally{ this.loading=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
openCreate(){ this.editId=null; this.error=''; this.form={nombre:'',tipo:'',documento:'',email:'',telefono:'',contacto:'',notas:'',activo:true}; this.showModal=true; },
|
||||||
|
openEdit(e){
|
||||||
|
this.editId=e.ID; this.error='';
|
||||||
|
this.form={nombre:e.nombre,tipo:e.tipo||'',documento:e.documento||'',email:e.email||'',telefono:e.telefono||'',contacto:e.contacto||'',notas:e.notas||'',activo:e.activo};
|
||||||
|
this.showModal=true;
|
||||||
|
},
|
||||||
|
async save(){
|
||||||
|
this.saving=true; this.error='';
|
||||||
|
try {
|
||||||
|
if(this.editId) await axios.put(`/app/contabilidad/entidades/${this.editId}`, this.form);
|
||||||
|
else await axios.post('/app/contabilidad/entidades', this.form);
|
||||||
|
this.showModal=false; await this.load();
|
||||||
|
} catch(e){ this.error=e.response?.data?.error||'Error al guardar'; }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
confirmDelete(e){ this.deleteId=e.ID; this.showDelete=true; },
|
||||||
|
async doDelete(){
|
||||||
|
this.saving=true;
|
||||||
|
try{ await axios.delete(`/app/contabilidad/entidades/${this.deleteId}`); this.showDelete=false; await this.load(); }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
<div x-data="transaccionesApp()" x-init="init()" class="p-6">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800">Transacciones</h1>
|
||||||
|
<p class="text-sm text-slate-500 mt-1">Registro de ingresos y egresos</p>
|
||||||
|
</div>
|
||||||
|
<button @click="openCreate()" class="btn-primary flex items-center gap-2">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/></svg>
|
||||||
|
Nueva transacción
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filtros -->
|
||||||
|
<div class="flex flex-wrap items-center gap-3 mb-4">
|
||||||
|
<input x-model="search" @input.debounce.400ms="page=1;load()" type="text" placeholder="Buscar..." class="input-field w-full max-w-xs">
|
||||||
|
<select x-model="filtroTipo" @change="page=1;load()" class="input-field">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
<option value="ingreso">Ingresos</option>
|
||||||
|
<option value="egreso">Egresos</option>
|
||||||
|
</select>
|
||||||
|
<select x-model="filtroMes" @change="page=1;load()" class="input-field">
|
||||||
|
<option value="0">Todos los meses</option>
|
||||||
|
<template x-for="(m,i) in meses" :key="i+1">
|
||||||
|
<option :value="i+1" x-text="m"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabla -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wide">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left">Fecha</th>
|
||||||
|
<th class="px-4 py-3 text-left">Tipo</th>
|
||||||
|
<th class="px-4 py-3 text-left">Descripción</th>
|
||||||
|
<th class="px-4 py-3 text-left">Categoría</th>
|
||||||
|
<th class="px-4 py-3 text-left">Entidad</th>
|
||||||
|
<th class="px-4 py-3 text-right">Valor</th>
|
||||||
|
<th class="px-4 py-3 text-left">Pago</th>
|
||||||
|
<th class="px-4 py-3 text-left">Acciones</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
<template x-if="loading">
|
||||||
|
<tr><td colspan="8" class="text-center py-10 text-slate-400">Cargando...</td></tr>
|
||||||
|
</template>
|
||||||
|
<template x-for="t in items" :key="t.ID">
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="px-4 py-3 text-xs text-slate-500" x-text="formatDate(t.fecha)"></td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span class="badge" :class="t.tipo==='ingreso'?'badge-green':'badge-red'" x-text="t.tipo"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-slate-700 max-w-xs truncate" x-text="t.descripcion"></td>
|
||||||
|
<td class="px-4 py-3 text-slate-600">
|
||||||
|
<span class="inline-block w-2 h-2 rounded-full mr-1" :style="'background:'+(t.cuenta?.color||'#ccc')"></span>
|
||||||
|
<span x-text="t.cuenta?.nombre||'-'"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-slate-600" x-text="t.entidad?.nombre||'-'"></td>
|
||||||
|
<td class="px-4 py-3 text-right font-semibold" :class="t.tipo==='ingreso'?'text-green-600':'text-red-600'" x-text="formatoCOP(t.valor)"></td>
|
||||||
|
<td class="px-4 py-3 text-xs text-slate-500" x-text="t.forma_pago||'-'"></td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button @click="openEdit(t)" class="btn-icon text-yellow-500">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
|
||||||
|
</button>
|
||||||
|
<button @click="confirmDelete(t)" class="btn-icon text-red-500">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Paginación -->
|
||||||
|
<div class="flex justify-between items-center mt-4 text-sm text-slate-500">
|
||||||
|
<span>Total: <strong x-text="total"></strong></span>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<button @click="page--;load()" :disabled="page<=1" class="px-3 py-1 rounded border border-slate-200 disabled:opacity-40">Ant</button>
|
||||||
|
<span class="px-3 py-1" x-text="`${page} / ${totalPages||1}`"></span>
|
||||||
|
<button @click="page++;load()" :disabled="page>=totalPages" class="px-3 py-1 rounded border border-slate-200 disabled:opacity-40">Sig</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal crear/editar -->
|
||||||
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6 max-h-screen overflow-y-auto">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4" x-text="editId ? 'Editar transacción' : 'Nueva transacción'"></h2>
|
||||||
|
<form @submit.prevent="save()">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Tipo</label>
|
||||||
|
<select x-model="form.tipo" class="input-field w-full" required>
|
||||||
|
<option value="ingreso">Ingreso</option>
|
||||||
|
<option value="egreso">Egreso</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Descripción</label>
|
||||||
|
<input x-model="form.descripcion" class="input-field w-full" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Valor ($)</label>
|
||||||
|
<input x-model.number="form.valor" type="number" step="0.01" class="input-field w-full" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Fecha</label>
|
||||||
|
<input x-model="form.fecha" type="date" class="input-field w-full">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Categoría</label>
|
||||||
|
<select x-model.number="form.cuenta_id" class="input-field w-full">
|
||||||
|
<option value="">Seleccionar...</option>
|
||||||
|
<template x-for="c in cuentas" :key="c.ID">
|
||||||
|
<option :value="c.ID" x-text="c.nombre"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Entidad</label>
|
||||||
|
<select x-model.number="form.entidad_id" class="input-field w-full">
|
||||||
|
<option value="">Seleccionar...</option>
|
||||||
|
<template x-for="e in entidades" :key="e.ID">
|
||||||
|
<option :value="e.ID" x-text="e.nombre"></option>
|
||||||
|
</template>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Forma de pago</label>
|
||||||
|
<select x-model="form.forma_pago" class="input-field w-full">
|
||||||
|
<option value="">Seleccionar...</option>
|
||||||
|
<option value="transferencia">Transferencia</option>
|
||||||
|
<option value="efectivo">Efectivo</option>
|
||||||
|
<option value="tarjeta">Tarjeta</option>
|
||||||
|
<option value="cheque">Cheque</option>
|
||||||
|
<option value="otro">Otro</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Estado</label>
|
||||||
|
<select x-model="form.estado" class="input-field w-full">
|
||||||
|
<option value="registrada">Registrada</option>
|
||||||
|
<option value="conciliada">Conciliada</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Notas</label>
|
||||||
|
<textarea x-model="form.notas" class="input-field w-full" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="error" x-text="error" class="text-red-500 text-sm mt-3"></p>
|
||||||
|
<div class="flex justify-end gap-3 mt-5">
|
||||||
|
<button type="button" @click="showModal=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button type="submit" :disabled="saving" class="btn-primary" x-text="saving?'Guardando...':'Guardar'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal eliminar -->
|
||||||
|
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
|
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-2">¿Eliminar transacción?</h2>
|
||||||
|
<p class="text-slate-600 text-sm mb-5">Esta acción no se puede deshacer.</p>
|
||||||
|
<div class="flex justify-end gap-3">
|
||||||
|
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
||||||
|
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function transaccionesApp() {
|
||||||
|
return {
|
||||||
|
items:[], cuentas:[], entidades:[], total:0, totalPages:1, page:1, search:'',
|
||||||
|
filtroTipo:'', filtroMes:0,
|
||||||
|
loading:false, saving:false, showModal:false, showDelete:false,
|
||||||
|
editId:null, deleteId:null, error:'',
|
||||||
|
meses:['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'],
|
||||||
|
form:{ tipo:'ingreso', descripcion:'', valor:0, fecha:'', cuenta_id:'', entidad_id:'', forma_pago:'', estado:'registrada', notas:'' },
|
||||||
|
|
||||||
|
async init(){ await this.load(); await this.loadSelects(); },
|
||||||
|
|
||||||
|
async load(){
|
||||||
|
this.loading=true;
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({page:this.page, search:this.search});
|
||||||
|
if(this.filtroTipo) params.set('tipo', this.filtroTipo);
|
||||||
|
if(this.filtroMes) { params.set('mes', this.filtroMes); params.set('anio', new Date().getFullYear()); }
|
||||||
|
const r=await axios.get('/app/contabilidad/transacciones/list?'+params.toString());
|
||||||
|
this.items=r.data.items; this.total=r.data.total; this.totalPages=r.data.totalPages;
|
||||||
|
} finally{ this.loading=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadSelects(){
|
||||||
|
try {
|
||||||
|
const [cr, er] = await Promise.all([
|
||||||
|
axios.get('/app/contabilidad/cuentas/select'),
|
||||||
|
axios.get('/app/contabilidad/entidades/select')
|
||||||
|
]);
|
||||||
|
this.cuentas=cr.data; this.entidades=er.data;
|
||||||
|
} catch(e){}
|
||||||
|
},
|
||||||
|
|
||||||
|
openCreate(){
|
||||||
|
this.editId=null; this.error='';
|
||||||
|
this.form={tipo:'ingreso',descripcion:'',valor:0,fecha:'',cuenta_id:'',entidad_id:'',forma_pago:'',estado:'registrada',notas:''};
|
||||||
|
this.showModal=true;
|
||||||
|
},
|
||||||
|
openEdit(t){
|
||||||
|
this.editId=t.ID; this.error='';
|
||||||
|
const f=t.fecha?t.fecha.substring(0,10):'';
|
||||||
|
this.form={tipo:t.tipo,descripcion:t.descripcion,valor:t.valor,fecha:f,cuenta_id:t.cuenta_id||'',entidad_id:t.entidad_id||'',forma_pago:t.forma_pago||'',estado:t.estado||'registrada',notas:t.notas||''};
|
||||||
|
this.showModal=true;
|
||||||
|
},
|
||||||
|
async save(){
|
||||||
|
this.saving=true; this.error='';
|
||||||
|
const payload={...this.form};
|
||||||
|
try {
|
||||||
|
if(this.editId) await axios.put(`/app/contabilidad/transacciones/${this.editId}`, payload);
|
||||||
|
else await axios.post('/app/contabilidad/transacciones', payload);
|
||||||
|
this.showModal=false; await this.load();
|
||||||
|
} catch(e){ this.error=e.response?.data?.error||'Error al guardar'; }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
confirmDelete(t){ this.deleteId=t.ID; this.showDelete=true; },
|
||||||
|
async doDelete(){
|
||||||
|
this.saving=true;
|
||||||
|
try{ await axios.delete(`/app/contabilidad/transacciones/${this.deleteId}`); this.showDelete=false; await this.load(); }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
formatoCOP(n){ if(n==null) return '$0'; return '$ '+Number(n).toLocaleString('es-CO',{minimumFractionDigits:0}); },
|
||||||
|
formatDate(d){ if(!d) return ''; return new Date(d).toLocaleDateString('es-CO',{day:'2-digit',month:'short',year:'numeric'}); },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<div x-data="websmsApp()" x-init="init()" class="p-6">
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold text-slate-800">WebSMS (LabsMobile)</h1>
|
||||||
|
<p class="text-sm text-slate-500 mt-1">Integración de SMS para notificaciones</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Configuración -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Configuración API</h2>
|
||||||
|
<form @submit.prevent="save()">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">Usuario (email)</label>
|
||||||
|
<input x-model="form.username" class="input-field w-full" required placeholder="lizandro.guarnizo@u-site.app">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="label">API Token</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input x-model="form.api_token" type="password" class="input-field w-full pr-10" required>
|
||||||
|
<button type="button" @click="showToken=!showToken; $el.previousElementSibling.type=showToken?'text':'password'" class="absolute right-2 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Remitente (TPOA)</label>
|
||||||
|
<input x-model="form.sender" class="input-field w-full" placeholder="U-SITE">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Notas</label>
|
||||||
|
<input x-model="form.notas" class="input-field w-full" placeholder="Opcional">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="error" x-text="error" class="text-red-500 text-sm mt-3"></p>
|
||||||
|
<div class="flex justify-end mt-5">
|
||||||
|
<button type="submit" :disabled="saving" class="btn-primary" x-text="saving?'Guardando...':'Guardar configuración'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Probar envío -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Probar envío</h2>
|
||||||
|
<form @submit.prevent="test()">
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="label">Número destino</label>
|
||||||
|
<input x-model="testPara" class="input-field w-full" placeholder="57300xxxxxx" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Mensaje (opcional)</label>
|
||||||
|
<input x-model="testMsg" class="input-field w-full" placeholder="Texto de prueba">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p x-show="testResult" x-text="testResult" class="text-sm mt-3" :class="testOk?'text-green-600':'text-red-500'"></p>
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<button type="submit" :disabled="saving" class="btn-primary" x-text="saving?'Enviando...':'Enviar SMS de prueba'"></button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Webhooks URLs -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Webhooks</h2>
|
||||||
|
<p class="text-sm text-slate-500 mb-4">Configura estas URLs en el panel de WebSMS para recibir notificaciones:</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="label">Confirmaciones de entrega (ACK)</label>
|
||||||
|
<code class="block text-xs bg-slate-50 p-2 rounded border border-slate-200 text-slate-700 break-all">https://admin.u-site.app/webhooks/websms/delivery</code>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Clics en shortlinks</label>
|
||||||
|
<code class="block text-xs bg-slate-50 p-2 rounded border border-slate-200 text-slate-700 break-all">https://admin.u-site.app/webhooks/websms/click</code>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="label">Mensajes entrantes</label>
|
||||||
|
<code class="block text-xs bg-slate-50 p-2 rounded border border-slate-200 text-slate-700 break-all">https://admin.u-site.app/webhooks/websms/incoming</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logs -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Últimos envíos</h2>
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead class="bg-slate-50 text-slate-500 text-xs uppercase tracking-wide">
|
||||||
|
<tr>
|
||||||
|
<th class="px-3 py-2 text-left">Fecha</th>
|
||||||
|
<th class="px-3 py-2 text-left">Para</th>
|
||||||
|
<th class="px-3 py-2 text-left">Mensaje</th>
|
||||||
|
<th class="px-3 py-2 text-left">Estado</th>
|
||||||
|
<th class="px-3 py-2 text-left">ID</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-slate-100">
|
||||||
|
<template x-for="l in logs" :key="l.ID">
|
||||||
|
<tr class="hover:bg-slate-50">
|
||||||
|
<td class="px-3 py-2 text-xs text-slate-500" x-text="formatDate(l.CreatedAt)"></td>
|
||||||
|
<td class="px-3 py-2 text-slate-700" x-text="l.para"></td>
|
||||||
|
<td class="px-3 py-2 text-slate-600 max-w-xs truncate" x-text="l.mensaje"></td>
|
||||||
|
<td class="px-3 py-2">
|
||||||
|
<span class="badge" :class="l.status==='0'?'badge-green':'badge-red'" x-text="l.status"></span>
|
||||||
|
</td>
|
||||||
|
<td class="px-3 py-2 text-xs font-mono text-slate-500" x-text="l.msg_id||'-'"></td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template x-if="!logs.length">
|
||||||
|
<tr><td colspan="5" class="text-center py-6 text-slate-400">Sin envíos aún</td></tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function websmsApp() {
|
||||||
|
return {
|
||||||
|
form:{ username:'', api_token:'', sender:'', notas:'' },
|
||||||
|
showToken:false,
|
||||||
|
logs:[],
|
||||||
|
testPara:'', testMsg:'', testResult:'', testOk:false,
|
||||||
|
saving:false, error:'',
|
||||||
|
|
||||||
|
async init(){
|
||||||
|
await this.loadConfig();
|
||||||
|
await this.loadLogs();
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadConfig(){
|
||||||
|
try {
|
||||||
|
const r=await axios.get('/app/websms/config');
|
||||||
|
if(r.data.data){
|
||||||
|
this.form={id:r.data.data.ID, username:r.data.data.username, api_token:r.data.data.api_token, sender:r.data.data.sender||'', notas:r.data.data.notas||''};
|
||||||
|
}
|
||||||
|
} catch(e){}
|
||||||
|
},
|
||||||
|
|
||||||
|
async loadLogs(){
|
||||||
|
try { const r=await axios.get('/app/websms/logs'); this.logs=r.data; } catch(e){}
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(){
|
||||||
|
this.saving=true; this.error='';
|
||||||
|
try {
|
||||||
|
await axios.post('/app/websms/save', this.form);
|
||||||
|
await this.loadConfig();
|
||||||
|
} catch(e){ this.error=e.response?.data?.error||'Error al guardar'; }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
async test(){
|
||||||
|
this.saving=true; this.testResult=''; this.testOk=false;
|
||||||
|
try {
|
||||||
|
const r=await axios.post('/app/websms/test', {para:this.testPara, mensaje:this.testMsg});
|
||||||
|
this.testResult='SMS enviado correctamente. Código: '+r.data.response.code;
|
||||||
|
this.testOk=true;
|
||||||
|
await this.loadLogs();
|
||||||
|
} catch(e){ this.testResult=e.response?.data?.error||'Error al enviar'; this.testOk=false; }
|
||||||
|
finally{ this.saving=false; }
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(d){ if(!d) return ''; return new Date(d).toLocaleDateString('es-CO',{day:'2-digit',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}); },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
</style>
|
||||||
@@ -120,3 +120,22 @@ function contabilidadApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.text-primary { color:#8eb02f; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-md mx-4 p-6">
|
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-md mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-4" x-text="editId ? 'Editar categoría' : 'Nueva categoría'"></h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-4" x-text="editId ? 'Editar categoría' : 'Nueva categoría'"></h2>
|
||||||
<form @submit.prevent="save()">
|
<form @submit.prevent="save()">
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
|
|
||||||
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-2">¿Eliminar categoría?</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-2">¿Eliminar categoría?</h2>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
||||||
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
||||||
@@ -148,3 +148,22 @@ function cuentasApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.text-primary { color:#8eb02f; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<!-- Modal crear -->
|
<!-- Modal crear -->
|
||||||
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-4">Nuevo cobro pendiente</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Nuevo cobro pendiente</h2>
|
||||||
<form @submit.prevent="save()">
|
<form @submit.prevent="save()">
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<!-- Modal pagado -->
|
<!-- Modal pagado -->
|
||||||
<div x-show="showPagarModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showPagarModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showPagarModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
<div @click.outside="showPagarModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-4">Marcar como pagado</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Marcar como pagado</h2>
|
||||||
<form @submit.prevent="doPagar()">
|
<form @submit.prevent="doPagar()">
|
||||||
<label class="label">Fecha de pago</label>
|
<label class="label">Fecha de pago</label>
|
||||||
<input x-model="pagoFecha" type="date" class="input-field w-full mb-4" required>
|
<input x-model="pagoFecha" type="date" class="input-field w-full mb-4" required>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
|
|
||||||
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-2">¿Eliminar?</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-2">¿Eliminar?</h2>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
||||||
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
||||||
@@ -192,3 +192,22 @@ function cobroApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.text-primary { color:#8eb02f; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
<!-- Modal crear -->
|
<!-- Modal crear -->
|
||||||
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-4">Nuevo pendiente de pago</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Nuevo pendiente de pago</h2>
|
||||||
<form @submit.prevent="save()">
|
<form @submit.prevent="save()">
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<!-- Modal pagado -->
|
<!-- Modal pagado -->
|
||||||
<div x-show="showPagarModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showPagarModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showPagarModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
<div @click.outside="showPagarModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-4">Marcar como pagado</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Marcar como pagado</h2>
|
||||||
<form @submit.prevent="doPagar()">
|
<form @submit.prevent="doPagar()">
|
||||||
<label class="label">Fecha de pago</label>
|
<label class="label">Fecha de pago</label>
|
||||||
<input x-model="pagoFecha" type="date" class="input-field w-full mb-4" required>
|
<input x-model="pagoFecha" type="date" class="input-field w-full mb-4" required>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
|
|
||||||
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-2">¿Eliminar?</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-2">¿Eliminar?</h2>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
||||||
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
||||||
@@ -192,3 +192,22 @@ function pagarApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.text-primary { color:#8eb02f; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-4" x-text="editId?'Editar entidad':'Nueva entidad'"></h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-4" x-text="editId?'Editar entidad':'Nueva entidad'"></h2>
|
||||||
<form @submit.prevent="save()">
|
<form @submit.prevent="save()">
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
|
|
||||||
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-2">¿Eliminar entidad?</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-2">¿Eliminar entidad?</h2>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
||||||
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
<button @click="doDelete()" :disabled="saving" class="btn-danger" x-text="saving?'Eliminando...':'Eliminar'"></button>
|
||||||
@@ -167,3 +167,22 @@ function entidadesApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.text-primary { color:#8eb02f; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -89,7 +89,7 @@
|
|||||||
<!-- Modal crear/editar -->
|
<!-- Modal crear/editar -->
|
||||||
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showModal" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6 max-h-screen overflow-y-auto">
|
<div @click.outside="showModal=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-lg mx-4 p-6 max-h-screen overflow-y-auto">
|
||||||
<h2 class="text-lg font-bold mb-4" x-text="editId ? 'Editar transacción' : 'Nueva transacción'"></h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-4" x-text="editId ? 'Editar transacción' : 'Nueva transacción'"></h2>
|
||||||
<form @submit.prevent="save()">
|
<form @submit.prevent="save()">
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
<!-- Modal eliminar -->
|
<!-- Modal eliminar -->
|
||||||
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
<div x-show="showDelete" x-cloak class="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
|
||||||
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
<div @click.outside="showDelete=false" class="bg-white rounded-2xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||||
<h2 class="text-lg font-bold mb-2">¿Eliminar transacción?</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-2">¿Eliminar transacción?</h2>
|
||||||
<p class="text-slate-500 text-sm mb-5">Esta acción no se puede deshacer.</p>
|
<p class="text-slate-500 text-sm mb-5">Esta acción no se puede deshacer.</p>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
<button @click="showDelete=false" class="btn-secondary">Cancelar</button>
|
||||||
@@ -242,3 +242,22 @@ function transaccionesApp() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-danger { background:#ef4444; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-blue { background:#dbeafe; color:#1d4ed8; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.text-primary { color:#8eb02f; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<div class="w-8 h-8 rounded-full flex-shrink-0 flex items-center justify-center text-xs font-bold"
|
<div class="w-8 h-8 rounded-full flex-shrink-0 flex items-center justify-center text-xs font-bold"
|
||||||
style="background:#8eb02f;color:#fff">
|
style="background:#8eb02f;color:#fff">
|
||||||
{{ if eq .Tipo "milestone" }}🎯{{ else if eq .Tipo "alerta" }}⚠️{{ else }}✓{{ end }}
|
{{ if eq .Tipo "milestone" }}🎯{{ else if eq .Tipo "alerta" }}⚠️{{ else if eq .Tipo "nota" }}📝{{ else if eq .Tipo "update" }}✨{{ else }}✓{{ end }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="font-semibold text-slate-800 text-sm">{{ .Titulo }}</h3>
|
<h3 class="font-semibold text-slate-800 text-sm">{{ .Titulo }}</h3>
|
||||||
|
|||||||
@@ -41,6 +41,19 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- API Key -->
|
||||||
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
|
||||||
|
<h2 class="text-lg font-bold text-slate-800 mb-1">API Key (envío externo)</h2>
|
||||||
|
<p class="text-sm text-slate-500 mb-4">Usa este token en <code class="bg-slate-100 px-1 rounded">Authorization: Bearer <key></code> para llamar a <code class="bg-slate-100 px-1 rounded">POST /api/sms/send</code> desde sistemas externos.</p>
|
||||||
|
<div x-show="secretKey" class="flex items-center gap-2">
|
||||||
|
<code class="flex-1 text-xs bg-slate-50 p-2 rounded border border-slate-200 text-slate-700 break-all font-mono" x-text="secretKey"></code>
|
||||||
|
<button type="button" @click="copy(secretKey)" class="btn-icon text-slate-400 hover:text-slate-600 flex-shrink-0" title="Copiar">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p x-show="!secretKey" class="text-sm text-slate-400">Guarda la configuración para generar la API Key.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Probar envío -->
|
<!-- Probar envío -->
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6 mb-6">
|
||||||
<h2 class="text-lg font-bold text-slate-800 mb-4">Probar envío</h2>
|
<h2 class="text-lg font-bold text-slate-800 mb-4">Probar envío</h2>
|
||||||
@@ -69,18 +82,34 @@
|
|||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="label">Confirmaciones de entrega (ACK)</label>
|
<label class="label">Confirmaciones de entrega (ACK)</label>
|
||||||
<code class="block text-xs bg-slate-50 p-2 rounded border border-slate-200 text-slate-700 break-all">https://admin.u-site.app/webhooks/websms/delivery</code>
|
<div class="flex items-center gap-2">
|
||||||
|
<code class="flex-1 text-xs bg-slate-50 p-2 rounded border border-slate-200 text-slate-700 break-all">https://admin.u-site.app/webhooks/websms/delivery</code>
|
||||||
|
<button type="button" @click="copy('https://admin.u-site.app/webhooks/websms/delivery')" class="btn-icon text-slate-400 hover:text-slate-600 flex-shrink-0" title="Copiar">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="label">Clics en shortlinks</label>
|
<label class="label">Clics en shortlinks</label>
|
||||||
<code class="block text-xs bg-slate-50 p-2 rounded border border-slate-200 text-slate-700 break-all">https://admin.u-site.app/webhooks/websms/click</code>
|
<div class="flex items-center gap-2">
|
||||||
|
<code class="flex-1 text-xs bg-slate-50 p-2 rounded border border-slate-200 text-slate-700 break-all">https://admin.u-site.app/webhooks/websms/click</code>
|
||||||
|
<button type="button" @click="copy('https://admin.u-site.app/webhooks/websms/click')" class="btn-icon text-slate-400 hover:text-slate-600 flex-shrink-0" title="Copiar">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="label">Mensajes entrantes</label>
|
<label class="label">Mensajes entrantes</label>
|
||||||
<code class="block text-xs bg-slate-50 p-2 rounded border border-slate-200 text-slate-700 break-all">https://admin.u-site.app/webhooks/websms/incoming</code>
|
<div class="flex items-center gap-2">
|
||||||
|
<code class="flex-1 text-xs bg-slate-50 p-2 rounded border border-slate-200 text-slate-700 break-all">https://admin.u-site.app/webhooks/websms/incoming</code>
|
||||||
|
<button type="button" @click="copy('https://admin.u-site.app/webhooks/websms/incoming')" class="btn-icon text-slate-400 hover:text-slate-600 flex-shrink-0" title="Copiar">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p x-show="copied" x-text="copied" class="text-green-600 text-xs mt-3"></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
||||||
@@ -103,7 +132,7 @@
|
|||||||
<td class="px-3 py-2 text-slate-700" x-text="l.para"></td>
|
<td class="px-3 py-2 text-slate-700" x-text="l.para"></td>
|
||||||
<td class="px-3 py-2 text-slate-600 max-w-xs truncate" x-text="l.mensaje"></td>
|
<td class="px-3 py-2 text-slate-600 max-w-xs truncate" x-text="l.mensaje"></td>
|
||||||
<td class="px-3 py-2">
|
<td class="px-3 py-2">
|
||||||
<span class="badge" :class="l.status==='0'?'badge-green':'badge-red'" x-text="l.status"></span>
|
<span class="badge" :class="l.status==='0'?'badge-green':'badge-red'" x-text="l.status==='0'?'Enviado':'Error'"></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-3 py-2 text-xs font-mono text-slate-500" x-text="l.msg_id||'-'"></td>
|
<td class="px-3 py-2 text-xs font-mono text-slate-500" x-text="l.msg_id||'-'"></td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -122,10 +151,10 @@
|
|||||||
function websmsApp() {
|
function websmsApp() {
|
||||||
return {
|
return {
|
||||||
form:{ username:'', api_token:'', sender:'', notas:'' },
|
form:{ username:'', api_token:'', sender:'', notas:'' },
|
||||||
showToken:false,
|
showToken:false, secretKey:'',
|
||||||
logs:[],
|
logs:[],
|
||||||
testPara:'', testMsg:'', testResult:'', testOk:false,
|
testPara:'', testMsg:'', testResult:'', testOk:false,
|
||||||
saving:false, error:'',
|
saving:false, error:'', copied:'',
|
||||||
|
|
||||||
async init(){
|
async init(){
|
||||||
await this.loadConfig();
|
await this.loadConfig();
|
||||||
@@ -137,6 +166,7 @@ function websmsApp() {
|
|||||||
const r=await axios.get('/app/websms/config');
|
const r=await axios.get('/app/websms/config');
|
||||||
if(r.data.data){
|
if(r.data.data){
|
||||||
this.form={id:r.data.data.ID, username:r.data.data.username, api_token:r.data.data.api_token, sender:r.data.data.sender||'', notas:r.data.data.notas||''};
|
this.form={id:r.data.data.ID, username:r.data.data.username, api_token:r.data.data.api_token, sender:r.data.data.sender||'', notas:r.data.data.notas||''};
|
||||||
|
this.secretKey=r.data.data.secret_key||'';
|
||||||
}
|
}
|
||||||
} catch(e){}
|
} catch(e){}
|
||||||
},
|
},
|
||||||
@@ -166,6 +196,24 @@ function websmsApp() {
|
|||||||
},
|
},
|
||||||
|
|
||||||
formatDate(d){ if(!d) return ''; return new Date(d).toLocaleDateString('es-CO',{day:'2-digit',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}); },
|
formatDate(d){ if(!d) return ''; return new Date(d).toLocaleDateString('es-CO',{day:'2-digit',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit'}); },
|
||||||
|
copy(text){ navigator.clipboard.writeText(text); this.copied='URL copiada'; setTimeout(()=>this.copied='', 2000); },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.btn-primary { background:#8eb02f; color:#fff; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:600; font-size:.875rem; transition:background .15s; }
|
||||||
|
.btn-primary:hover { background:#6d8c24; }
|
||||||
|
.btn-secondary { background:#f1f5f9; color:#475569; padding:0.5rem 1rem; border-radius:0.5rem; font-weight:500; font-size:.875rem; border:1px solid #e2e8f0; }
|
||||||
|
.btn-icon { display:inline-flex; align-items:center; padding:0.25rem; border-radius:0.375rem; }
|
||||||
|
.btn-icon:hover { background:#f1f5f9; }
|
||||||
|
.input-field { border:1px solid #e2e8f0; border-radius:0.5rem; padding:0.5rem 0.75rem; font-size:.875rem; outline:none; transition:border-color .15s; }
|
||||||
|
.input-field:focus { border-color:#8eb02f; }
|
||||||
|
.label { display:block; font-size:.75rem; font-weight:600; color:#475569; margin-bottom:.25rem; text-transform:uppercase; letter-spacing:.05em; }
|
||||||
|
.badge { display:inline-block; padding:.15rem .6rem; border-radius:9999px; font-size:.7rem; font-weight:600; text-transform:capitalize; }
|
||||||
|
.badge-green { background:#dcfce7; color:#15803d; }
|
||||||
|
.badge-yellow { background:#fef9c3; color:#854d0e; }
|
||||||
|
.badge-red { background:#fee2e2; color:#991b1b; }
|
||||||
|
.badge-slate { background:#f1f5f9; color:#475569; }
|
||||||
|
.text-primary { color:#8eb02f; }
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ func PortalProyecto(c *fiber.Ctx) error {
|
|||||||
fases, _ := models.GetFasesByProyecto(proy.ID)
|
fases, _ := models.GetFasesByProyecto(proy.ID)
|
||||||
avances, _ := models.GetAvancesByProyecto(proy.ID, true)
|
avances, _ := models.GetAvancesByProyecto(proy.ID, true)
|
||||||
entregables, _ := models.GetEntregablesByProyecto(proy.ID, true)
|
entregables, _ := models.GetEntregablesByProyecto(proy.ID, true)
|
||||||
tickets, _ := models.GetTicketsByPortalUser(u.ID)
|
tickets, _ := models.GetTicketsByProyecto(proy.ID)
|
||||||
_ = models.ActualizarProgresoProyecto(proy.ID)
|
_ = models.ActualizarProgresoProyecto(proy.ID)
|
||||||
|
|
||||||
return c.Render("portal/proyecto", fiber.Map{
|
return c.Render("portal/proyecto", fiber.Map{
|
||||||
@@ -268,7 +268,7 @@ func PortalGetProyectoData(c *fiber.Ctx) error {
|
|||||||
fases, _ := models.GetFasesByProyecto(proy.ID)
|
fases, _ := models.GetFasesByProyecto(proy.ID)
|
||||||
avances, _ := models.GetAvancesByProyecto(proy.ID, true)
|
avances, _ := models.GetAvancesByProyecto(proy.ID, true)
|
||||||
entregables, _ := models.GetEntregablesByProyecto(proy.ID, true)
|
entregables, _ := models.GetEntregablesByProyecto(proy.ID, true)
|
||||||
tickets, _ := models.GetTicketsByPortalUser(u.ID)
|
tickets, _ := models.GetTicketsByProyecto(proy.ID)
|
||||||
facturas, _ := models.GetFacturasByCliente(proy.ClienteID, true)
|
facturas, _ := models.GetFacturasByCliente(proy.ClienteID, true)
|
||||||
facturasNoLeidas := models.CountUnreadFacturaNotifs(u.ID)
|
facturasNoLeidas := models.CountUnreadFacturaNotifs(u.ID)
|
||||||
documentos, _ := models.GetDocumentosByProyecto(proy.ID)
|
documentos, _ := models.GetDocumentosByProyecto(proy.ID)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
||||||
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/services"
|
||||||
@@ -146,6 +148,57 @@ func WebSmsClickWebhook(c *fiber.Ctx) error {
|
|||||||
return c.SendStatus(200)
|
return c.SendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── API pública ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// ApiSendSms envía un SMS. Requiere Authorization: Bearer <secret_key>.
|
||||||
|
// POST /api/sms/send { "numero": "573001234567", "mensaje": "Hola" }
|
||||||
|
func ApiSendSms(c *fiber.Ctx) error {
|
||||||
|
bearer := strings.TrimPrefix(c.Get("Authorization"), "Bearer ")
|
||||||
|
if bearer == "" {
|
||||||
|
return c.Status(401).JSON(fiber.Map{"error": "token requerido"})
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := models.GetWebSmsConfig()
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(503).JSON(fiber.Map{"error": "WebSMS no configurado"})
|
||||||
|
}
|
||||||
|
if cfg.SecretKey == "" || cfg.SecretKey != bearer {
|
||||||
|
return c.Status(401).JSON(fiber.Map{"error": "token inválido"})
|
||||||
|
}
|
||||||
|
|
||||||
|
type body struct {
|
||||||
|
Numero string `json:"numero"`
|
||||||
|
Mensaje string `json:"mensaje"`
|
||||||
|
}
|
||||||
|
var b body
|
||||||
|
if err := c.BodyParser(&b); err != nil || b.Numero == "" || b.Mensaje == "" {
|
||||||
|
return c.Status(400).JSON(fiber.Map{"error": "numero y mensaje son requeridos"})
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := services.SendWebSms(cfg, b.Numero, b.Mensaje)
|
||||||
|
errStr := ""
|
||||||
|
status := "ok"
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
status = "error"
|
||||||
|
} else if resp != nil {
|
||||||
|
status = resp.Code
|
||||||
|
}
|
||||||
|
|
||||||
|
models.CreateWebSmsLog(&models.WebSmsLog{
|
||||||
|
Para: b.Numero,
|
||||||
|
Mensaje: b.Mensaje,
|
||||||
|
Status: status,
|
||||||
|
MsgID: func() string { if resp != nil { return resp.ID }; return "" }(),
|
||||||
|
Error: errStr,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(500).JSON(fiber.Map{"error": errStr})
|
||||||
|
}
|
||||||
|
return c.JSON(fiber.Map{"ok": true, "id": resp.ID})
|
||||||
|
}
|
||||||
|
|
||||||
func WebSmsIncomingWebhook(c *fiber.Ctx) error {
|
func WebSmsIncomingWebhook(c *fiber.Ctx) error {
|
||||||
var payload services.WebSmsIncomingPayload
|
var payload services.WebSmsIncomingPayload
|
||||||
if err := c.BodyParser(&payload); err != nil {
|
if err := c.BodyParser(&payload); err != nil {
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ func RutasPublicas(web fiber.Router) {
|
|||||||
// Configurar en el bot: POST https://api.telegram.org/bot{TOKEN}/setWebhook?url={HOST}/webhooks/telegram-portal
|
// Configurar en el bot: POST https://api.telegram.org/bot{TOKEN}/setWebhook?url={HOST}/webhooks/telegram-portal
|
||||||
web.Post("/webhooks/telegram-portal", controllers.TelegramPortalWebhook)
|
web.Post("/webhooks/telegram-portal", controllers.TelegramPortalWebhook)
|
||||||
|
|
||||||
// ─── WebSMS (LabsMobile) — ACK de entrega, clics y mensajes entrantes ──
|
// ─── WebSMS (LabsMobile) — API de envío + webhooks ──────────────────
|
||||||
|
web.Post("/api/sms/send", controllers.ApiSendSms)
|
||||||
web.Post("/webhooks/websms/delivery", controllers.WebSmsDeliveryWebhook)
|
web.Post("/webhooks/websms/delivery", controllers.WebSmsDeliveryWebhook)
|
||||||
web.Post("/webhooks/websms/click", controllers.WebSmsClickWebhook)
|
web.Post("/webhooks/websms/click", controllers.WebSmsClickWebhook)
|
||||||
web.Post("/webhooks/websms/incoming", controllers.WebSmsIncomingWebhook)
|
web.Post("/webhooks/websms/incoming", controllers.WebSmsIncomingWebhook)
|
||||||
|
|||||||
@@ -453,6 +453,49 @@ func UserRoutes(app fiber.Router) {
|
|||||||
protected.Put("/mis-notifs/:id/leida", controllers.MarcarNotifLeida)
|
protected.Put("/mis-notifs/:id/leida", controllers.MarcarNotifLeida)
|
||||||
protected.Post("/mis-notifs/marcar-todas", controllers.MarcarTodasLeidas)
|
protected.Post("/mis-notifs/marcar-todas", controllers.MarcarTodasLeidas)
|
||||||
|
|
||||||
|
// ─── Contabilidad ─────────────────────────────────────────────────────────
|
||||||
|
protected.Get("/contabilidad", middlewares.MenuMiddleware, controllers.ContabilidadIndex)
|
||||||
|
protected.Get("/contabilidad/dashboard", controllers.ContabilidadDashboard)
|
||||||
|
protected.Get("/contabilidad/consolidado", controllers.ContabilidadConsolidado)
|
||||||
|
protected.Get("/contabilidad/consolidados", controllers.ContabilidadListConsolidados)
|
||||||
|
|
||||||
|
// Transacciones
|
||||||
|
protected.Get("/contabilidad/transacciones", middlewares.MenuMiddleware, controllers.ContabilidadTransaccionesView)
|
||||||
|
protected.Get("/contabilidad/transacciones/list", controllers.GetTransacciones)
|
||||||
|
protected.Post("/contabilidad/transacciones", controllers.CreateTransaccion)
|
||||||
|
protected.Put("/contabilidad/transacciones/:id", controllers.UpdateTransaccion)
|
||||||
|
protected.Delete("/contabilidad/transacciones/:id", controllers.DeleteTransaccion)
|
||||||
|
|
||||||
|
// Cuentas (categorías contables)
|
||||||
|
protected.Get("/contabilidad/cuentas", middlewares.MenuMiddleware, controllers.ContabilidadCuentasView)
|
||||||
|
protected.Get("/contabilidad/cuentas/list", controllers.GetCuentas)
|
||||||
|
protected.Get("/contabilidad/cuentas/select", controllers.GetCuentasSelect)
|
||||||
|
protected.Post("/contabilidad/cuentas", controllers.CreateCuenta)
|
||||||
|
protected.Put("/contabilidad/cuentas/:id", controllers.UpdateCuenta)
|
||||||
|
protected.Delete("/contabilidad/cuentas/:id", controllers.DeleteCuenta)
|
||||||
|
|
||||||
|
// Entidades
|
||||||
|
protected.Get("/contabilidad/entidades", middlewares.MenuMiddleware, controllers.ContabilidadEntidadesView)
|
||||||
|
protected.Get("/contabilidad/entidades/list", controllers.GetEntidades)
|
||||||
|
protected.Get("/contabilidad/entidades/select", controllers.GetEntidadesSelect)
|
||||||
|
protected.Post("/contabilidad/entidades", controllers.CreateEntidad)
|
||||||
|
protected.Put("/contabilidad/entidades/:id", controllers.UpdateEntidad)
|
||||||
|
protected.Delete("/contabilidad/entidades/:id", controllers.DeleteEntidad)
|
||||||
|
|
||||||
|
// Cuentas por cobrar
|
||||||
|
protected.Get("/contabilidad/cuentas-cobro", middlewares.MenuMiddleware, controllers.ContabilidadCobroView)
|
||||||
|
protected.Get("/contabilidad/cuentas-cobro/list", controllers.GetCuentasCobro)
|
||||||
|
protected.Post("/contabilidad/cuentas-cobro", controllers.CreateCuentaCobro)
|
||||||
|
protected.Put("/contabilidad/cuentas-cobro/:id", controllers.UpdateCuentaCobro)
|
||||||
|
protected.Delete("/contabilidad/cuentas-cobro/:id", controllers.DeleteCuentaCobro)
|
||||||
|
|
||||||
|
// Cuentas por pagar
|
||||||
|
protected.Get("/contabilidad/cuentas-pagar", middlewares.MenuMiddleware, controllers.ContabilidadPagarView)
|
||||||
|
protected.Get("/contabilidad/cuentas-pagar/list", controllers.GetCuentasPagar)
|
||||||
|
protected.Post("/contabilidad/cuentas-pagar", controllers.CreateCuentaPagar)
|
||||||
|
protected.Put("/contabilidad/cuentas-pagar/:id", controllers.UpdateCuentaPagar)
|
||||||
|
protected.Delete("/contabilidad/cuentas-pagar/:id", controllers.DeleteCuentaPagar)
|
||||||
|
|
||||||
// ─── WebSMS (LabsMobile) ───────────────────────────────────────────────────
|
// ─── WebSMS (LabsMobile) ───────────────────────────────────────────────────
|
||||||
protected.Get("/websms", middlewares.MenuMiddleware, controllers.WebSmsConfigPage)
|
protected.Get("/websms", middlewares.MenuMiddleware, controllers.WebSmsConfigPage)
|
||||||
protected.Get("/websms/config", controllers.GetWebSmsConfig)
|
protected.Get("/websms/config", controllers.GetWebSmsConfig)
|
||||||
|
|||||||
Reference in New Issue
Block a user