fix: tickets filtrados por proyecto, vistas contabilidad/websms en resources/view
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
<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>
|
||||
@@ -0,0 +1,150 @@
|
||||
<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">Nueva categoría</button>
|
||||
</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-md mx-4 p-6">
|
||||
<h2 class="text-lg font-bold mb-4" x-text="editId ? 'Editar categoría' : 'Nueva categoría'"></h2>
|
||||
<form @submit.prevent="save()">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<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="input-field w-full h-10">
|
||||
</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 mb-2">¿Eliminar categoría?</h2>
|
||||
<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,
|
||||
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}`);
|
||||
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>
|
||||
@@ -0,0 +1,194 @@
|
||||
<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">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'">✓</button>
|
||||
<button @click="confirmDelete(c)" class="btn-icon text-red-500">🗑️</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 mb-4">Nuevo cobro pendiente</h2>
|
||||
<form @submit.prevent="save()">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<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 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">
|
||||
<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 mb-2">¿Eliminar?</h2>
|
||||
<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>
|
||||
@@ -0,0 +1,194 @@
|
||||
<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">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'">✓</button>
|
||||
<button @click="confirmDelete(c)" class="btn-icon text-red-500">🗑️</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 mb-4">Nuevo pendiente de pago</h2>
|
||||
<form @submit.prevent="save()">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<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 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">
|
||||
<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 mb-2">¿Eliminar?</h2>
|
||||
<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>
|
||||
@@ -0,0 +1,169 @@
|
||||
<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">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">✏️</button>
|
||||
<button @click="confirmDelete(e)" class="btn-icon text-red-500">🗑️</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 mb-4" x-text="editId?'Editar entidad':'Nueva entidad'"></h2>
|
||||
<form @submit.prevent="save()">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<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 mb-2">¿Eliminar entidad?</h2>
|
||||
<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>
|
||||
@@ -0,0 +1,244 @@
|
||||
<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 mb-4" x-text="editId ? 'Editar transacción' : 'Nueva transacción'"></h2>
|
||||
<form @submit.prevent="save()">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<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 mb-2">¿Eliminar transacción?</h2>
|
||||
<p class="text-slate-500 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>
|
||||
@@ -0,0 +1,171 @@
|
||||
<div x-data="websmsApp()" x-init="init()" class="p-6 max-w-3xl mx-auto">
|
||||
|
||||
<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>
|
||||
@@ -210,7 +210,7 @@ func PortalProyecto(c *fiber.Ctx) error {
|
||||
fases, _ := models.GetFasesByProyecto(proy.ID)
|
||||
avances, _ := models.GetAvancesByProyecto(proy.ID, true)
|
||||
entregables, _ := models.GetEntregablesByProyecto(proy.ID, true)
|
||||
tickets, _ := models.GetTicketsByPortalUser(u.ID)
|
||||
tickets, _ := models.GetTicketsByProyecto(proy.ID)
|
||||
_ = models.ActualizarProgresoProyecto(proy.ID)
|
||||
|
||||
return c.Render("portal/proyecto", fiber.Map{
|
||||
@@ -268,7 +268,7 @@ func PortalGetProyectoData(c *fiber.Ctx) error {
|
||||
fases, _ := models.GetFasesByProyecto(proy.ID)
|
||||
avances, _ := models.GetAvancesByProyecto(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)
|
||||
facturasNoLeidas := models.CountUnreadFacturaNotifs(u.ID)
|
||||
documentos, _ := models.GetDocumentosByProyecto(proy.ID)
|
||||
|
||||
@@ -453,6 +453,49 @@ func UserRoutes(app fiber.Router) {
|
||||
protected.Put("/mis-notifs/:id/leida", controllers.MarcarNotifLeida)
|
||||
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) ───────────────────────────────────────────────────
|
||||
protected.Get("/websms", middlewares.MenuMiddleware, controllers.WebSmsConfigPage)
|
||||
protected.Get("/websms/config", controllers.GetWebSmsConfig)
|
||||
|
||||
Reference in New Issue
Block a user