mirror of https://github.com/Lukibeg/OmniBoard.git
Compare commits
No commits in common. "523f5080d3d8034ba4502f0af1ff917dc6224368" and "7e88f3b6f25dfbc760acafc9ead2ad306220a83c" have entirely different histories.
523f5080d3
...
7e88f3b6f2
|
|
@ -26,11 +26,8 @@ public function handle(Request $request)
|
||||||
$data = $request->all();
|
$data = $request->all();
|
||||||
$eventName = $data['Event'] ?? null;
|
$eventName = $data['Event'] ?? null;
|
||||||
$queueNumber = $data['Queue'] ?? null;
|
$queueNumber = $data['Queue'] ?? null;
|
||||||
|
$interface = $data['Interface'] ?? $data['MemberName'] ?? null; // Tenta achar quem é o agente
|
||||||
|
|
||||||
// Tenta achar a interface. Se for MemberStatus, vem em 'Interface'. Se for AgentConnect, vem em 'Interface' ou 'MemberName'
|
|
||||||
$interface = $data['Interface'] ?? $data['MemberName'] ?? null;
|
|
||||||
|
|
||||||
// Auto-discovery de Filas e Agentes
|
|
||||||
if ($queueNumber) {
|
if ($queueNumber) {
|
||||||
$this->saveQueues($queueNumber, $tenant);
|
$this->saveQueues($queueNumber, $tenant);
|
||||||
}
|
}
|
||||||
|
|
@ -42,187 +39,52 @@ public function handle(Request $request)
|
||||||
return response()->json(['status' => 'ignored']);
|
return response()->json(['status' => 'ignored']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recupera Fila
|
$queue = null;
|
||||||
|
if ($queueNumber) {
|
||||||
$queue = Queue::where('tenant_id', $tenant->id)
|
$queue = Queue::where('tenant_id', $tenant->id)
|
||||||
->where('source_id', $queueNumber)
|
->where('source_id', $queueNumber)
|
||||||
->first();
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recupera o Agente (se aplicável)
|
||||||
|
$agent = null;
|
||||||
|
if ($interface) {
|
||||||
|
$agent = Agent::where('tenant_id', $tenant->id)
|
||||||
|
->where('interface', $interface)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
if (!$queue) {
|
if (!$queue) {
|
||||||
return response()->json(['error' => 'Fila não encontrada'], 404);
|
return response()->json(['error' => 'Fila não encontrada'], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recupera Agente (se aplicável e se não for canal Local)
|
|
||||||
$agent = $this->findAgent($tenant->id, $data);
|
|
||||||
|
|
||||||
|
|
||||||
switch ($eventName) {
|
switch ($eventName) {
|
||||||
case 'QueueCallerJoin':
|
case 'QueueCallerJoin':
|
||||||
$this->handleJoin($queue, $data);
|
$this->handleJoin($queue, $data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'AgentConnect':
|
case 'AgentConnect':
|
||||||
// Foco: Mudar para 'talking' e atualizar métricas
|
|
||||||
$this->handleConnect($queue, $data);
|
$this->handleConnect($queue, $data);
|
||||||
break;
|
break;
|
||||||
|
case 'AgentComplete': // Ou QueueCallerLeave com TalkTime
|
||||||
case 'AgentComplete':
|
|
||||||
// Foco: Encerrar contagem de tempo de fala
|
|
||||||
$this->handleComplete($queue, $data);
|
$this->handleComplete($queue, $data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'QueueCallerAbandon':
|
case 'QueueCallerAbandon':
|
||||||
$this->handleAbandon($queue, $data);
|
$this->handleAbandon($queue, $data);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'QueueMemberPause':
|
case 'QueueMemberPause':
|
||||||
// Evento explícito de pausa (Alguém clicou no botão de pausa)
|
|
||||||
if ($agent) $this->handlePause($agent, $data, $tenant->id);
|
if ($agent) $this->handlePause($agent, $data, $tenant->id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'AgentRingNoAnswer':
|
case 'AgentRingNoAnswer':
|
||||||
if ($agent) $this->handleRingNoAnswer($agent, $tenant->id);
|
if ($agent) $this->handleRingNoAnswer($agent, $tenant->id);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'QueueMemberStatus':
|
|
||||||
// Foco: Atualizar Presença (Offline, Disponível, Pausado, Busy)
|
|
||||||
if ($agent) $this->handleMemberStatus($agent, $data, $tenant->id);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(['status' => 'success']);
|
return response()->json(['status' => 'success']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
|
||||||
// HANDLERS
|
|
||||||
// =========================================================================
|
|
||||||
|
|
||||||
private function handleMemberStatus($agent, $data, $tenantId)
|
|
||||||
{
|
|
||||||
// 1. A PAUSA TEM PRIORIDADE ABSOLUTA
|
|
||||||
// Se o Asterisk diz que Paused é '1', o agente está pausado,
|
|
||||||
// não importa se o telefone está no gancho ou fora do gancho.
|
|
||||||
if (($data['Paused'] ?? '0') == '1') {
|
|
||||||
$agent->status = 'paused';
|
|
||||||
|
|
||||||
// Só atualizamos o motivo se o evento trouxer um motivo novo.
|
|
||||||
// O QueueMemberStatus as vezes não traz o motivo, então evitamos apagar o existente.
|
|
||||||
if (!empty($data['PausedReason'])) {
|
|
||||||
$agent->pause_reason = $data['PausedReason'];
|
|
||||||
} elseif (!$agent->pause_reason) {
|
|
||||||
$agent->pause_reason = 'Pausa'; // Fallback se estiver nulo
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// 2. SE NÃO ESTÁ PAUSADO, OLHAMOS O STATUS DO DISPOSITIVO
|
|
||||||
$agent->pause_reason = null;
|
|
||||||
|
|
||||||
$asteriskStatus = $data['Status'] ?? null;
|
|
||||||
|
|
||||||
switch ($asteriskStatus) {
|
|
||||||
case '1': // AST_DEVICE_NOT_INUSE (Livre)
|
|
||||||
$agent->status = 'available';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '2': // AST_DEVICE_INUSE (Em uso / Falando)
|
|
||||||
case '3': // AST_DEVICE_BUSY (Ocupado)
|
|
||||||
case '6': // AST_DEVICE_RINGING (Tocando)
|
|
||||||
// Aqui mantemos 'talking' para ficar azul no dashboard
|
|
||||||
$agent->status = 'talking';
|
|
||||||
break;
|
|
||||||
|
|
||||||
case '5': // AST_DEVICE_UNAVAILABLE (Offline / Cabo desconectado / Softphone fechado)
|
|
||||||
$agent->status = 'offline';
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Se vier status desconhecido (4, 0, etc), mantemos o atual ou definimos offline
|
|
||||||
// $agent->status = 'offline';
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$agent->last_status_change = now();
|
|
||||||
$agent->save();
|
|
||||||
|
|
||||||
broadcast(new DashboardUpdate($tenantId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function handleConnect($queue, $data)
|
|
||||||
{
|
|
||||||
// 1. Remove da Lista de Espera
|
|
||||||
WaitingList::where('queue_id', $queue->id)
|
|
||||||
->where('caller_number', $data['CallerIDNum'])
|
|
||||||
->delete();
|
|
||||||
|
|
||||||
// 2. Atualiza Métricas da Fila
|
|
||||||
$metric = $this->getTodayMetric($queue);
|
|
||||||
$holdTime = intval($data['HoldTime'] ?? 0);
|
|
||||||
$newAvg = (($metric->avg_wait_time * $metric->answered_count) + $holdTime) / ($metric->answered_count + 1);
|
|
||||||
|
|
||||||
$metric->avg_wait_time = $newAvg;
|
|
||||||
$metric->answered_count += 1;
|
|
||||||
$metric->save();
|
|
||||||
|
|
||||||
// 3. Atualiza Agente para Talking (Feedback Visual Imediato)
|
|
||||||
$agent = $this->findAgent($queue->tenant_id, $data);
|
|
||||||
if ($agent) {
|
|
||||||
$agent->status = 'talking';
|
|
||||||
$agent->last_status_change = now();
|
|
||||||
$agent->increment('total_calls_answered');
|
|
||||||
$agent->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->broadcastUpdate($queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function handleComplete($queue, $data)
|
|
||||||
{
|
|
||||||
// Atualiza tempo médio falado
|
|
||||||
$talkTime = intval($data['TalkTime'] ?? 0);
|
|
||||||
$metric = $this->getTodayMetric($queue);
|
|
||||||
|
|
||||||
if ($metric->answered_count > 0) {
|
|
||||||
$newAvg = (($metric->avg_talk_time * ($metric->answered_count - 1)) + $talkTime) / $metric->answered_count;
|
|
||||||
$metric->avg_talk_time = $newAvg;
|
|
||||||
$metric->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTA: Removemos a lógica de mudar status para 'available' aqui.
|
|
||||||
// Deixamos o 'QueueMemberStatus' cuidar disso quando o telefone for colocado no gancho.
|
|
||||||
// Isso evita conflitos se o agente desligar e imediatamente receber outra chamada.
|
|
||||||
|
|
||||||
$this->broadcastUpdate($queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function handlePause($agent, $data, $tenantId)
|
|
||||||
{
|
|
||||||
// Esse evento ocorre quando a ação de pausa é disparada
|
|
||||||
$isPaused = isset($data['Paused']) && $data['Paused'] == '1';
|
|
||||||
|
|
||||||
$agent->status = $isPaused ? 'paused' : 'available';
|
|
||||||
$agent->pause_reason = $isPaused ? ($data['Reason'] ?? 'Pausa') : null;
|
|
||||||
$agent->last_status_change = now();
|
|
||||||
|
|
||||||
$agent->save();
|
|
||||||
|
|
||||||
broadcast(new DashboardUpdate($tenantId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function handleAbandon($queue, $data)
|
|
||||||
{
|
|
||||||
WaitingList::where('queue_id', $queue->id)
|
|
||||||
->where('caller_number', $data['CallerIDNum'])
|
|
||||||
->delete();
|
|
||||||
|
|
||||||
$this->updateMetric($queue, 'abandoned_count', 1);
|
|
||||||
$this->broadcastUpdate($queue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function handleRingNoAnswer($agent, $tenantId)
|
|
||||||
{
|
|
||||||
$agent->increment('total_ring_no_answer');
|
|
||||||
broadcast(new DashboardUpdate($tenantId));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function handleJoin($queue, $data)
|
private function handleJoin($queue, $data)
|
||||||
{
|
{
|
||||||
|
|
@ -238,23 +100,94 @@ private function handleJoin($queue, $data)
|
||||||
$this->broadcastUpdate($queue);
|
$this->broadcastUpdate($queue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
private function handleConnect($queue, $data)
|
||||||
// AUXILIARES
|
{
|
||||||
// =========================================================================
|
// 1. Remove da Lista de Espera (Já existente)
|
||||||
|
WaitingList::where('queue_id', $queue->id)
|
||||||
|
->where('caller_number', $data['CallerIDNum'])
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
// 2. Atualiza Métricas da Fila (Já existente)
|
||||||
|
$metric = $this->getTodayMetric($queue);
|
||||||
|
$holdTime = intval($data['HoldTime'] ?? 0);
|
||||||
|
$newAvg = (($metric->avg_wait_time * $metric->answered_count) + $holdTime) / ($metric->answered_count + 1);
|
||||||
|
|
||||||
|
$metric->avg_wait_time = $newAvg;
|
||||||
|
$metric->answered_count += 1;
|
||||||
|
$metric->save();
|
||||||
|
|
||||||
|
// --- NOVO: Atualiza o Agente ---
|
||||||
|
$agent = $this->findAgent($queue->tenant_id, $data);
|
||||||
|
if ($agent) {
|
||||||
|
$agent->status = 'talking'; // Muda cor para Azul
|
||||||
|
$agent->last_status_change = now();
|
||||||
|
$agent->increment('total_calls_answered'); // KPI pessoal do agente
|
||||||
|
$agent->save();
|
||||||
|
}
|
||||||
|
// -------------------------------
|
||||||
|
|
||||||
|
$this->broadcastUpdate($queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleComplete($queue, $data)
|
||||||
|
{
|
||||||
|
// 1. Atualiza Métricas da Fila (Já existente)
|
||||||
|
$talkTime = intval($data['TalkTime'] ?? 0);
|
||||||
|
$metric = $this->getTodayMetric($queue);
|
||||||
|
|
||||||
|
if ($metric->answered_count > 0) {
|
||||||
|
$newAvg = (($metric->avg_talk_time * ($metric->answered_count - 1)) + $talkTime) / $metric->answered_count;
|
||||||
|
$metric->avg_talk_time = $newAvg;
|
||||||
|
$metric->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- NOVO: Libera o Agente ---
|
||||||
|
$agent = $this->findAgent($queue->tenant_id, $data);
|
||||||
|
if ($agent) {
|
||||||
|
// Só volta para available se ele não estiver pausado (fluxo simples)
|
||||||
|
// O ideal seria checar se ele pausou DURANTE a chamada, mas
|
||||||
|
// geralmente ao desligar ele volta a ficar livre.
|
||||||
|
$agent->status = 'available'; // Muda cor para Verde
|
||||||
|
$agent->last_status_change = now();
|
||||||
|
$agent->save();
|
||||||
|
}
|
||||||
|
// -----------------------------
|
||||||
|
|
||||||
|
$this->broadcastUpdate($queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handleAbandon($queue, $data)
|
||||||
|
{
|
||||||
|
WaitingList::where('queue_id', $queue->id)
|
||||||
|
->where('caller_number', $data['CallerIDNum'])
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$this->updateMetric($queue, 'abandoned_count', 1);
|
||||||
|
$this->broadcastUpdate($queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function handlePause($agent, $data, $tenantId)
|
||||||
|
{
|
||||||
|
$isPaused = isset($data['Paused']) && $data['Paused'] == '1';
|
||||||
|
|
||||||
|
// Atualiza o Status ENUM e o Motivo
|
||||||
|
$agent->status = $isPaused ? 'paused' : 'available'; // Define se está pausado ou disponível
|
||||||
|
$agent->pause_reason = $isPaused ? ($data['Reason'] ?? 'Pausa') : null;
|
||||||
|
$agent->last_status_change = now(); // Atualiza o relógio
|
||||||
|
|
||||||
|
$agent->save();
|
||||||
|
|
||||||
|
broadcast(new DashboardUpdate($tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
private function saveAgent($interface, $tenant)
|
private function saveAgent($interface, $tenant)
|
||||||
{
|
{
|
||||||
// Ignora canais locais para não sujar o banco
|
|
||||||
if (str_starts_with($interface, 'Local/')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$exists = Agent::where('tenant_id', $tenant->id)
|
$exists = Agent::where('tenant_id', $tenant->id)
|
||||||
->where('interface', $interface)
|
->where('interface', $interface)
|
||||||
->exists();
|
->exists();
|
||||||
|
|
||||||
if (!$exists) {
|
if (!$exists) {
|
||||||
// Lógica de extração de nome
|
// Tenta limpar o nome. Ex: PJSIP/200 -> 200
|
||||||
$name = $interface;
|
$name = $interface;
|
||||||
if (strpos($interface, '/') !== false) {
|
if (strpos($interface, '/') !== false) {
|
||||||
$parts = explode('/', $interface);
|
$parts = explode('/', $interface);
|
||||||
|
|
@ -265,42 +198,11 @@ private function saveAgent($interface, $tenant)
|
||||||
'tenant_id' => $tenant->id,
|
'tenant_id' => $tenant->id,
|
||||||
'name' => "Agente $name",
|
'name' => "Agente $name",
|
||||||
'interface' => $interface,
|
'interface' => $interface,
|
||||||
'status' => 'available'
|
'status' => 'available' // Assume disponível ao ser descoberto
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function saveQueues($queueSourceId, $tenant)
|
|
||||||
{
|
|
||||||
$exists = Queue::where('tenant_id', $tenant->id)
|
|
||||||
->where('source_id', $queueSourceId)
|
|
||||||
->exists();
|
|
||||||
|
|
||||||
if (!$exists) {
|
|
||||||
Queue::create([
|
|
||||||
'tenant_id' => $tenant->id,
|
|
||||||
'type' => 'voice',
|
|
||||||
'source_id' => $queueSourceId,
|
|
||||||
'name' => "Fila $queueSourceId",
|
|
||||||
'sector' => null
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function findAgent($tenantId, $data)
|
|
||||||
{
|
|
||||||
$interface = $data['Interface'] ?? $data['MemberName'] ?? null;
|
|
||||||
|
|
||||||
// Se for canal Local/ ou nulo, retorna null para não processar
|
|
||||||
if (!$interface || str_starts_with($interface, 'Local/')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Agent::where('tenant_id', $tenantId)
|
|
||||||
->where('interface', $interface)
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getTodayMetric($queue)
|
private function getTodayMetric($queue)
|
||||||
{
|
{
|
||||||
return DailyMetric::firstOrCreate(
|
return DailyMetric::firstOrCreate(
|
||||||
|
|
@ -323,6 +225,43 @@ private function updateMetric($queue, $field, $value)
|
||||||
$metric->increment($field, $value);
|
$metric->increment($field, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function handleRingNoAnswer($agent, $tenantId)
|
||||||
|
{
|
||||||
|
// Incrementa contador de chamadas perdidas pelo agente
|
||||||
|
$agent->increment('total_ring_no_answer');
|
||||||
|
|
||||||
|
broadcast(new DashboardUpdate($tenantId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function saveQueues($queue, $tenant)
|
||||||
|
{
|
||||||
|
$existingQueue = Queue::where('tenant_id', $tenant->id)
|
||||||
|
->where('source_id', $queue)
|
||||||
|
->exists();
|
||||||
|
|
||||||
|
if (!$existingQueue) {
|
||||||
|
Queue::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'type' => 'voice',
|
||||||
|
'source_id' => $queue,
|
||||||
|
'name' => "Fila $queue", // Nome padrão provisório
|
||||||
|
'sector' => null // Começa sem setor
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- COLOQUE AQUI NO FINAL, JUNTO COM OS OUTROS PRIVATES ---
|
||||||
|
private function findAgent($tenantId, $data)
|
||||||
|
{
|
||||||
|
// Tenta achar o identificador do agente no evento
|
||||||
|
$interface = $data['Interface'] ?? $data['MemberName'] ?? null;
|
||||||
|
|
||||||
|
if (!$interface) return null;
|
||||||
|
|
||||||
|
return Agent::where('tenant_id', $tenantId)
|
||||||
|
->where('interface', $interface)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
private function broadcastUpdate($queueOrTenantId)
|
private function broadcastUpdate($queueOrTenantId)
|
||||||
{
|
{
|
||||||
$tenantId = is_object($queueOrTenantId) ? $queueOrTenantId->tenant_id : $queueOrTenantId;
|
$tenantId = is_object($queueOrTenantId) ? $queueOrTenantId->tenant_id : $queueOrTenantId;
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,12 @@
|
||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Traits\BelongsToTenant;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class Agent extends Model
|
class Agent extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, BelongsToTenant;
|
use HasFactory;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'tenant_id',
|
'tenant_id',
|
||||||
|
|
@ -25,10 +24,4 @@ class Agent extends Model
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'last_status_change' => 'datetime',
|
'last_status_change' => 'datetime',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Opcional, mas recomendado: Relacionamento inverso
|
|
||||||
public function tenant()
|
|
||||||
{
|
|
||||||
return $this->belongsTo(Tenant::class);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue