mirror of https://github.com/Lukibeg/OmniBoard.git
151 lines
6.5 KiB
Vue
151 lines
6.5 KiB
Vue
<script setup>
|
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
|
import { Head, router } from '@inertiajs/vue3';
|
|
import { onMounted, onUnmounted } from 'vue';
|
|
|
|
const props = defineProps({
|
|
agents: Array,
|
|
auth: Object
|
|
});
|
|
|
|
// Configuração do WebSocket para atualização Real-Time
|
|
onMounted(() => {
|
|
const tenantChannelId = props.auth.user.tenant_id || null;
|
|
|
|
if (tenantChannelId === null) {
|
|
console.error('Tenant ID não encontrado para WebSocket');
|
|
return;
|
|
}
|
|
|
|
console.log(`📡 Conectando agentes ao canal: dashboard.${tenantChannelId}`);
|
|
|
|
window.Echo.private(`dashboard.${tenantChannelId}`)
|
|
// CORREÇÃO AQUI: Usar o mesmo nome definido no broadcastAs()
|
|
// O ponto '.' no início previne que o Laravel adicione o namespace App\Events
|
|
.listen('.metrics.updated', (e) => {
|
|
console.log("🔔 Atualização de Agentes recebida!", e);
|
|
|
|
// Recarrega apenas a prop 'agents' sem piscar a tela
|
|
router.reload({ only: ['agents'], preserveScroll: true });
|
|
})
|
|
.error((error) => {
|
|
console.error('❌ Erro no WebSocket de Agentes:', error);
|
|
});
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
const tenantChannelId = props.auth.user.tenant_id;
|
|
if (tenantChannelId) {
|
|
window.Echo.leave(`dashboard.${tenantChannelId}`);
|
|
}
|
|
});
|
|
|
|
// Função auxiliar para cores de status
|
|
const getStatusColor = (status) => {
|
|
switch (status) {
|
|
case 'available': return 'bg-green-100 text-green-800 border-green-200';
|
|
case 'paused': return 'bg-yellow-100 text-yellow-800 border-yellow-200';
|
|
case 'talking': return 'bg-blue-100 text-blue-800 border-blue-200';
|
|
default: return 'bg-gray-100 text-gray-800 border-gray-200'; // offline
|
|
}
|
|
};
|
|
|
|
const getStatusDot = (status) => {
|
|
switch (status) {
|
|
case 'available': return 'bg-green-500';
|
|
case 'paused': return 'bg-yellow-500';
|
|
case 'talking': return 'bg-blue-500';
|
|
default: return 'bg-gray-400';
|
|
}
|
|
};
|
|
|
|
const translateStatus = (status) => {
|
|
switch (status) {
|
|
case 'available': return 'Disponível';
|
|
case 'paused': return 'Em Pausa';
|
|
case 'talking': return 'Em Chamada';
|
|
case 'offline': return 'Offline';
|
|
default: return status;
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
|
|
<Head title="Agentes" />
|
|
|
|
<AuthenticatedLayout>
|
|
<template #header>
|
|
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
|
|
Monitoramento de Agentes
|
|
</h2>
|
|
</template>
|
|
|
|
<div class="py-12">
|
|
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
|
|
|
|
<div v-if="agents.length > 0"
|
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
|
|
<div v-for="agent in agents" :key="agent.id"
|
|
class="bg-white overflow-hidden shadow-sm sm:rounded-lg border border-gray-100 transition hover:shadow-md">
|
|
|
|
<div class="p-6">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<div>
|
|
<h3 class="text-lg font-bold text-gray-800 truncate" :title="agent.name">
|
|
{{ agent.name }}
|
|
</h3>
|
|
<p class="text-xs text-gray-400 font-mono">{{ agent.interface }}</p>
|
|
</div>
|
|
<div :class="`h-3 w-3 rounded-full ${getStatusDot(agent.status)} animate-pulse`"></div>
|
|
</div>
|
|
|
|
<div class="flex justify-center mb-4">
|
|
<span
|
|
:class="`px-3 py-1 rounded-full text-xs font-semibold border ${getStatusColor(agent.status)}`">
|
|
{{ translateStatus(agent.status) }}
|
|
<span v-if="agent.status === 'paused' && agent.pause_reason"
|
|
class="block text-[10px] text-center font-normal pt-0.5 uppercase">
|
|
{{ agent.pause_reason }}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div class="text-center text-xs text-gray-400 mb-4">
|
|
{{ agent.status === 'offline' ? 'Visto por último:' : 'Neste status há:' }} {{
|
|
agent.status_duration }}
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-2 pt-4 border-t border-gray-50">
|
|
<div class="text-center">
|
|
<span class="block text-xl font-bold text-gray-700">{{ agent.calls_answered
|
|
}}</span>
|
|
<span class="text-[10px] text-gray-400 uppercase tracking-wider">Atendidas</span>
|
|
</div>
|
|
<div class="text-center border-l border-gray-50">
|
|
<span class="block text-xl font-bold text-red-500">{{ agent.calls_missed }}</span>
|
|
<span class="text-[10px] text-gray-400 uppercase tracking-wider">Perdidas</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div v-else
|
|
class="flex flex-col items-center justify-center p-12 bg-white rounded-lg shadow-sm border border-dashed border-gray-300">
|
|
<svg class="w-12 h-12 text-gray-300 mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z">
|
|
</path>
|
|
</svg>
|
|
<p class="text-gray-500 text-lg">Nenhum agente detectado ainda.</p>
|
|
<p class="text-gray-400 text-sm">Faça login nos ramais ou aguarde eventos do Asterisk.</p>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
</template> |