up
This commit is contained in:
@@ -49,23 +49,27 @@
|
||||
<svg class="w-4 h-4 inline" 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>
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 flex items-center gap-1">
|
||||
<template x-if="f.archivo">
|
||||
<a :href="`/app/facturas/${f.ID}/download`" class="btn-icon text-blue-500" title="Descargar">
|
||||
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||
</a>
|
||||
</template>
|
||||
<button @click="openUpload(f)" class="btn-icon text-slate-500" title="Subir PDF">
|
||||
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l4-4m0 0l4 4m-4-4v12"/></svg>
|
||||
</button>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-1">
|
||||
<template x-if="f.archivo">
|
||||
<a :href="`/app/facturas/${f.ID}/download`" class="btn-icon text-blue-500" title="Descargar">
|
||||
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||
</a>
|
||||
</template>
|
||||
<button @click="openUpload(f)" class="btn-icon text-slate-500" title="Subir PDF">
|
||||
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l4-4m0 0l4 4m-4-4v12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 flex items-center gap-2">
|
||||
<button @click="openEdit(f)" 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(f)" 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>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="openEdit(f)" 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(f)" 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>
|
||||
|
||||
@@ -25,12 +25,15 @@
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b border-slate-200 mb-6 gap-0 overflow-x-auto">
|
||||
<template x-for="tab in ['Roadmap','Avances','Entregables','Tickets','Facturas']" :key="tab">
|
||||
<button @click="activeTab=tab; if(tab==='Tickets') marcarTicketsPendientesLeidos()" class="relative px-4 py-2 text-sm whitespace-nowrap border-b-2 -mb-px transition-colors"
|
||||
<button @click="activeTab=tab; if(tab==='Tickets') marcarTicketsPendientesLeidos(); if(tab==='Facturas') marcarFacturasLeidas()" class="relative px-4 py-2 text-sm whitespace-nowrap border-b-2 -mb-px transition-colors"
|
||||
:class="activeTab===tab ? 'font-semibold border-[#8eb02f] text-[#8eb02f]' : 'border-transparent text-slate-500 hover:text-slate-700'">
|
||||
<span x-text="tab"></span>
|
||||
<template x-if="tab === 'Tickets' && ticketsNoLeidos > 0">
|
||||
<span class="absolute -top-1 -right-1 min-w-[18px] h-[18px] bg-red-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center px-1" x-text="ticketsNoLeidos > 9 ? '9+' : ticketsNoLeidos"></span>
|
||||
</template>
|
||||
<template x-if="tab === 'Facturas' && facturasNoLeidas > 0">
|
||||
<span class="absolute -top-1 -right-1 min-w-[18px] h-[18px] bg-amber-500 text-white text-[10px] font-bold rounded-full flex items-center justify-center px-1" x-text="facturasNoLeidas > 9 ? '9+' : facturasNoLeidas"></span>
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
@@ -212,31 +215,32 @@
|
||||
<!-- ─── FACTURAS ──────────────────────────────────────────────────────────── -->
|
||||
<div x-show="activeTab==='Facturas'" x-cloak>
|
||||
<div class="space-y-3">
|
||||
{{ range .facturas }}
|
||||
<div class="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-semibold text-sm text-slate-800">{{ if .Numero }}{{ .Numero }}{{ else }}Factura #{{ .ID }}{{ end }}</span>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full font-semibold capitalize
|
||||
{{ if eq .Estado "pagada" }}bg-green-100 text-green-700
|
||||
{{ else if eq .Estado "vencida" }}bg-red-100 text-red-700
|
||||
{{ else if eq .Estado "cancelada" }}bg-slate-100 text-slate-500
|
||||
{{ else }}bg-yellow-100 text-yellow-700{{ end }}">{{ .Estado }}</span>
|
||||
<template x-for="f in facturas" :key="f.ID">
|
||||
<div class="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
<span class="font-semibold text-sm text-slate-800" x-text="f.numero || ('Factura #' + f.ID)"></span>
|
||||
<span class="text-xs px-2 py-0.5 rounded-full font-semibold capitalize"
|
||||
:class="{
|
||||
'bg-green-100 text-green-700': f.estado === 'pagada',
|
||||
'bg-red-100 text-red-700': f.estado === 'vencida',
|
||||
'bg-slate-100 text-slate-500': f.estado === 'cancelada',
|
||||
'bg-yellow-100 text-yellow-700': !['pagada','vencida','cancelada'].includes(f.estado)
|
||||
}" x-text="f.estado"></span>
|
||||
<span x-show="f._nuevo" class="text-[10px] font-bold bg-amber-500 text-white px-1.5 py-0.5 rounded-full">Nuevo</span>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500" x-text="f.descripcion"></p>
|
||||
<p class="text-sm font-bold text-slate-700 mt-1" x-text="(f.moneda || '') + ' ' + Number(f.monto || 0).toLocaleString('es-CO')"></p>
|
||||
<p x-show="f.fecha_vencimiento" class="text-xs text-slate-400" x-text="'Vence: ' + formatDate(f.fecha_vencimiento)"></p>
|
||||
</div>
|
||||
<p class="text-xs text-slate-500">{{ .Descripcion }}</p>
|
||||
<p class="text-sm font-bold text-slate-700 mt-1">{{ .Moneda }} {{ .Monto }}</p>
|
||||
{{ if .FechaVencimiento }}<p class="text-xs text-slate-400">Vence: {{ .FechaVencimiento.Format "02 Jan 2006" }}</p>{{ end }}
|
||||
<template x-if="f.archivo">
|
||||
<a :href="`/portal/facturas/${f.ID}/download`" class="text-[#8eb02f] hover:text-[#6d8c24] flex-shrink-0" title="Descargar factura">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||
</a>
|
||||
</template>
|
||||
</div>
|
||||
{{ if .Archivo }}
|
||||
<a href="/portal/facturas/{{ .ID }}/download" class="text-[#8eb02f] hover:text-[#6d8c24]">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
|
||||
</a>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if not .facturas }}
|
||||
<p class="text-slate-400 text-sm text-center py-10">Sin facturas disponibles.</p>
|
||||
{{ end }}
|
||||
</template>
|
||||
<p x-show="!facturas.length && !loadingTickets" class="text-slate-400 text-sm text-center py-10">Sin facturas disponibles.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -253,9 +257,19 @@ function portalProyecto() {
|
||||
loadingTickets: false,
|
||||
creandoTicket: false,
|
||||
ticketForm: { titulo:'', descripcion:'', prioridad:'media' },
|
||||
facturas: [],
|
||||
facturasNoLeidas: 0,
|
||||
|
||||
async init() {
|
||||
// Soporte de URL param ?tab=Facturas
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const tabParam = params.get('tab');
|
||||
if (tabParam) this.activeTab = tabParam;
|
||||
await this.loadTickets();
|
||||
// Si la tab activa es Facturas, marcar como leídas automáticamente
|
||||
if (this.activeTab === 'Facturas') {
|
||||
this.marcarFacturasLeidas();
|
||||
}
|
||||
},
|
||||
|
||||
async loadTickets() {
|
||||
@@ -269,6 +283,12 @@ function portalProyecto() {
|
||||
_noLeidos: (t.mensajes||[]).filter(m => m.es_admin && !m.leido_portal).length,
|
||||
}));
|
||||
this.recalcNoLeidos();
|
||||
// Cargar facturas y conteo de no leídas
|
||||
this.facturasNoLeidas = r.data.facturasNoLeidas || 0;
|
||||
this.facturas = (r.data.facturas||[]).map(f => ({
|
||||
...f,
|
||||
_nuevo: this.facturasNoLeidas > 0,
|
||||
}));
|
||||
} catch(e){} finally { this.loadingTickets=false; }
|
||||
},
|
||||
|
||||
@@ -302,6 +322,14 @@ function portalProyecto() {
|
||||
this.recalcNoLeidos();
|
||||
},
|
||||
|
||||
async marcarFacturasLeidas() {
|
||||
if (this.facturasNoLeidas > 0) {
|
||||
this.facturasNoLeidas = 0;
|
||||
this.facturas.forEach(f => { f._nuevo = false; });
|
||||
axios.post('/portal/mis-notifs/marcar-facturas').catch(()=>{});
|
||||
}
|
||||
},
|
||||
|
||||
async crearTicket() {
|
||||
this.creandoTicket=true;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user