OmniBoard/resources/js/Pages/Dashboard.vue

198 lines
12 KiB
Vue

<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import { Head, router, usePage } from '@inertiajs/vue3';
import { onMounted, onUnmounted, computed, reactive } from 'vue';
import AgentListCompact from '@/Components/AgentListCompact.vue';
const props = defineProps({
queues: Array,
agents: Array
});
// === LÓGICA DE FILAS (SETORES) ===
const collapsedSectors = reactive(new Set());
const toggleSector = (name) => collapsedSectors.has(name) ? collapsedSectors.delete(name) : collapsedSectors.add(name);
const isExpanded = (name) => !collapsedSectors.has(name);
const queuesBySector = computed(() => {
const groups = {};
props.queues.forEach(q => {
const sector = (q.sector || 'Geral').toUpperCase();
if (!groups[sector]) groups[sector] = [];
groups[sector].push(q);
});
return Object.keys(groups).sort().reduce((obj, key) => { obj[key] = groups[key]; return obj; }, {});
});
// === KPIs ===
const kpis = computed(() => {
let waiting = 0, answered = 0, abandoned = 0;
props.queues.forEach(q => {
waiting += q.waiting_list?.length || 0;
const m = q.daily_metrics[0] || {};
answered += m.answered_count || 0;
abandoned += m.abandoned_count || 0;
});
const sla = (answered + abandoned) > 0 ? Math.round((answered / (answered + abandoned)) * 100) : 100;
return { waiting, answered, abandoned, sla };
});
// === WEBSOCKET ===
const page = usePage();
const userTenantId = page.props.auth.user.tenant_id;
onMounted(() => {
if (userTenantId && window.Echo) {
window.Echo.private(`dashboard.${userTenantId}`)
.listen('.metrics.updated', (e) => {
router.reload({ only: ['queues', 'agents'], preserveScroll: true });
});
}
});
onUnmounted(() => {
if (userTenantId) window.Echo.leave(`dashboard.${userTenantId}`);
});
</script>
<template>
<Head title="Monitoramento" />
<AuthenticatedLayout>
<template #header>
<div class="flex justify-between items-end">
<div>
<h2 class="font-bold text-2xl text-gray-800 leading-tight tracking-tight">
Dashboard
<p class="text-sm text-gray-500 mt-1">Visão operacional em tempo real</p>
</h2>
</div>
<div class="flex items-center gap-2 px-3 py-1 bg-green-50 text-green-700 rounded-full border border-green-100 shadow-sm mb-3.5">
<span class="relative flex h-2.5 w-2.5">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2.5 w-2.5 bg-green-500"></span>
</span>
<span class="text-xs font-bold uppercase tracking-wide">Live</span>
</div>
</div>
</template>
<div class="py-8 max-w-[1600px] mx-auto px-4 sm:px-6 lg:px-8 space-y-8">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div class="bg-white p-5 rounded-xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-gray-100 relative overflow-hidden group">
<div class="relative z-10">
<p class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-1">Em Espera</p>
<p class="text-4xl font-extrabold" :class="kpis.waiting > 0 ? 'text-red-500 animate-pulse' : 'text-gray-800'">
{{ kpis.waiting }}
</p>
</div>
<div class="absolute right-0 top-0 h-full w-1 bg-red-500 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</div>
<div class="bg-white p-5 rounded-xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border border-gray-100 relative overflow-hidden group">
<p class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-1">Atendidas</p>
<p class="text-4xl font-extrabold text-green-600">{{ kpis.answered }}</p>
<div class="absolute right-0 top-0 h-full w-1 bg-green-500 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</div>
<div class="bg-white p-5 rounded-xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border relative overflow-hidden group">
<p class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-1">Abandonadas</p>
<p class="text-4xl font-extrabold text-gray-700">{{ kpis.abandoned }}</p>
<div class="absolute right-0 top-0 h-full w-1 bg-gray-500 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</div>
<div class="bg-white p-5 rounded-xl shadow-[0_2px_10px_-3px_rgba(6,81,237,0.1)] border relative overflow-hidden group">
<p class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-1">Taxa SLA</p>
<p class="text-4xl font-extrabold text-indigo-600">{{ kpis.sla }}%</p>
<div class="absolute right-0 top-0 h-full w-1 bg-indigo-500 opacity-0 group-hover:opacity-100 transition-opacity"></div>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-4 gap-6 items-start">
<div class="xl:col-span-3 space-y-6">
<div v-for="(queues, sector) in queuesBySector" :key="sector"
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div @click="toggleSector(sector)"
class="bg-gray-50/50 px-5 py-3 border-b border-gray-100 cursor-pointer flex justify-between items-center hover:bg-gray-100 transition select-none group">
<div class="flex items-center gap-3">
<span class="h-2 w-2 rounded-full bg-indigo-500 group-hover:scale-125 transition-transform"></span>
<h3 class="font-bold text-gray-700">{{ sector }}</h3>
</div>
<svg class="w-5 h-5 text-gray-400 transform transition-transform duration-200"
:class="{ 'rotate-180': isExpanded(sector) }"
fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</div>
<div v-show="isExpanded(sector)" class="divide-y divide-gray-50">
<div v-for="queue in queues" :key="queue.id"
class="p-5 hover:bg-indigo-50/30 transition-colors flex flex-col md:flex-row justify-between items-center gap-4">
<div class="flex-1 flex items-center gap-4 w-full">
<div class="h-10 w-10 rounded-lg bg-indigo-100 text-indigo-600 flex items-center justify-center font-bold text-sm shadow-sm">
{{ queue.source_id }}
</div>
<div>
<h4 class="font-bold text-gray-800 text-lg">{{ queue.name }}</h4>
<div class="flex gap-2 text-xs mt-1">
<span class="text-gray-400">Voz</span>
<span class="text-gray-300">•</span>
<span class="text-gray-400">Prioridade Normal</span>
</div>
</div>
</div>
<div class="flex items-center justify-between w-full md:w-auto gap-8">
<div class="text-center min-w-[80px]">
<span class="block text-3xl font-extrabold leading-none transition-colors duration-300"
:class="(queue.waiting_list?.length || 0) > 0 ? 'text-red-500 scale-110' : 'text-gray-200'">
{{ queue.waiting_list?.length || 0 }}
</span>
<span class="text-[10px] font-bold text-gray-400 uppercase tracking-wide">Esperando</span>
</div>
<div class="grid grid-cols-3 gap-4 border-l border-gray-100 pl-6">
<div class="text-center">
<span class="block text-lg font-bold text-gray-700">
{{ queue.daily_metrics[0]?.received_count || 0 }}
</span>
<span class="text-[9px] text-gray-400 uppercase font-semibold">Total</span>
</div>
<div class="text-center">
<span class="block text-lg font-bold text-green-600">
{{ queue.daily_metrics[0]?.answered_count || 0 }}
</span>
<span class="text-[9px] text-gray-400 uppercase font-semibold">Atendidas</span>
</div>
<div class="text-center">
<span class="block text-lg font-bold text-red-400">
{{ queue.daily_metrics[0]?.abandoned_count || 0 }}
</span>
<span class="text-[9px] text-gray-400 uppercase font-semibold">Abandonadas</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div v-if="props.queues.length === 0" class="text-center py-12 bg-white rounded-xl border border-dashed border-gray-300">
<p class="text-gray-400">Nenhuma fila configurada neste tenant.</p>
</div>
</div>
<div class="xl:col-span-1 sticky top-6">
<AgentListCompact :agents="agents" />
</div>
</div>
</div>
</AuthenticatedLayout>
</template>