up
This commit is contained in:
@@ -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'); }
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user