This commit is contained in:
Lizandro Guarnizo
2026-05-16 18:47:40 -05:00
parent b6203c1a68
commit 4c14f4a641
5 changed files with 125 additions and 28 deletions
+67 -14
View File
@@ -25,9 +25,13 @@
<!-- 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" class="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'"
x-text="tab"></button>
<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"
: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>
</button>
</template>
</div>
@@ -159,12 +163,17 @@
<!-- Lista de tickets -->
<div class="space-y-3">
<template x-for="t in tickets" :key="t.ID">
<div class="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div class="p-4 flex items-start justify-between cursor-pointer" @click="t._open=!t._open">
<div class="bg-white rounded-xl border overflow-hidden" :class="t._noLeidos > 0 ? 'border-[#8eb02f]' : 'border-slate-200'">
<div class="p-4 flex items-start justify-between cursor-pointer" @click="abrirTicket(t)">
<div>
<div class="flex items-center gap-2 mb-1">
<span class="badge" :class="ticketBadge(t.estado)" x-text="t.estado"></span>
<span class="badge" :class="prioBadge(t.prioridad)" x-text="t.prioridad"></span>
<template x-if="t._noLeidos > 0">
<span class="inline-flex items-center gap-1 text-[10px] font-bold bg-red-500 text-white px-1.5 py-0.5 rounded-full">
<span x-text="t._noLeidos"></span> nuevo<span x-show="t._noLeidos > 1">s</span>
</span>
</template>
</div>
<h4 class="font-semibold text-slate-800 text-sm" x-text="t.titulo"></h4>
<p class="text-xs text-slate-400" x-text="formatDate(t.CreatedAt)"></p>
@@ -172,22 +181,25 @@
<svg class="w-4 h-4 text-slate-400 transition-transform" :class="t._open ? 'rotate-180' : ''" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/></svg>
</div>
<div x-show="t._open" class="border-t border-slate-100 px-4 pb-4">
<p class="text-sm text-slate-600 py-3 border-b border-slate-100" x-text="t.descripcion"></p>
<div class="space-y-2 mt-3">
<p class="text-sm text-slate-600 py-3 border-b border-slate-100 leading-relaxed" x-text="t.descripcion"></p>
<!-- Mensajes con scroll -->
<div class="overflow-y-auto space-y-2 mt-3 pr-1" style="max-height:260px" x-ref="msgBox">
<template x-for="m in (t.mensajes||[])" :key="m.ID">
<div class="flex" :class="m.es_admin ? 'justify-start' : 'justify-end'">
<div class="rounded-xl px-3 py-2 text-sm max-w-xs"
<div class="rounded-xl px-3 py-2 text-sm max-w-[75%]"
:class="m.es_admin ? 'bg-slate-100 text-slate-700' : 'text-white'" :style="m.es_admin ? '' : 'background:#8eb02f'">
<div class="text-xs font-semibold mb-0.5" x-text="m.autor_nombre" :style="m.es_admin ? 'color:#64748b' : 'color:rgba(255,255,255,.8)'"></div>
<span x-text="m.contenido"></span>
<p class="leading-snug break-words" x-text="m.contenido"></p>
<p class="text-[10px] mt-0.5 opacity-60 text-right" x-text="formatDate(m.CreatedAt)"></p>
</div>
</div>
</template>
</div>
<template x-if="t.estado !== 'cerrado' && t.estado !== 'resuelto'">
<div class="flex gap-2 mt-3">
<input x-model="t._reply" type="text" placeholder="Responder..." class="input-field flex-1 text-sm">
<button @click="responderTicket(t)" class="btn-primary text-xs">Enviar</button>
<div class="flex gap-2 mt-3 pt-3 border-t border-slate-100">
<input x-model="t._reply" type="text" placeholder="Responder..." class="input-field flex-1 text-sm"
@keydown.enter.prevent="responderTicket(t)">
<button @click="responderTicket(t)" class="btn-primary text-xs px-4">Enviar</button>
</div>
</template>
</div>
@@ -237,6 +249,7 @@ function portalProyecto() {
return {
activeTab: 'Roadmap',
tickets: [],
ticketsNoLeidos: 0,
loadingTickets: false,
creandoTicket: false,
ticketForm: { titulo:'', descripcion:'', prioridad:'media' },
@@ -249,15 +262,51 @@ function portalProyecto() {
this.loadingTickets = true;
try {
const r = await axios.get(`/portal/api/proyecto/${PROYECTO_SLUG}`);
this.tickets = (r.data.tickets||[]).map(t => ({...t, _open:false, _reply:''}));
this.tickets = (r.data.tickets||[]).map(t => ({
...t,
_open: false,
_reply: '',
_noLeidos: (t.mensajes||[]).filter(m => m.es_admin && !m.leido_portal).length,
}));
this.recalcNoLeidos();
} catch(e){} finally { this.loadingTickets=false; }
},
recalcNoLeidos() {
this.ticketsNoLeidos = this.tickets.reduce((sum, t) => sum + (t._noLeidos||0), 0);
},
async abrirTicket(t) {
t._open = !t._open;
if (t._open) {
if (t._noLeidos > 0) {
t._noLeidos = 0;
this.recalcNoLeidos();
axios.post(`/portal/tickets/${t.ID}/leido`).catch(()=>{});
}
// scroll al final de mensajes al abrir
await this.$nextTick();
const boxes = document.querySelectorAll('[x-ref="msgBox"]');
boxes.forEach(b => { b.scrollTop = b.scrollHeight; });
}
},
async marcarTicketsPendientesLeidos() {
// Al abrir el tab Tickets, si alguno está abierto y tiene no leídos, marcarlos
this.tickets.forEach(t => {
if (t._open && t._noLeidos > 0) {
t._noLeidos = 0;
axios.post(`/portal/tickets/${t.ID}/leido`).catch(()=>{});
}
});
this.recalcNoLeidos();
},
async crearTicket() {
this.creandoTicket=true;
try {
const r = await axios.post('/portal/tickets', { ...this.ticketForm, proyecto_slug: PROYECTO_SLUG });
this.tickets.unshift({...r.data, _open:false, _reply:''});
this.tickets.unshift({...r.data, _open:false, _reply:'', _noLeidos:0});
this.ticketForm={titulo:'',descripcion:'',prioridad:'media'};
} catch(e) {
alert(e.response?.data?.error || 'Error al crear ticket');
@@ -271,6 +320,10 @@ function portalProyecto() {
if(!t.mensajes) t.mensajes=[];
t.mensajes.push(r.data);
t._reply='';
// scroll al último mensaje
await this.$nextTick();
const boxes = document.querySelectorAll('[x-ref="msgBox"]');
boxes.forEach(b => { b.scrollTop = b.scrollHeight; });
} catch(e) { alert(e.response?.data?.error||'Error'); }
},