Compare commits

..

No commits in common. "ae264024907a2a28b17172675ab4a246d281ce2d" and "2747b97286a1d5d7321748c4936bbaa44badbc25" have entirely different histories.

8 changed files with 53 additions and 209 deletions

View File

@ -24,23 +24,20 @@ public function handle(Request $request)
$data = $request->all(); $data = $request->all();
$eventName = $data['Event'] ?? null; $eventName = $data['Event'] ?? null;
$queueNumber = $data['Queue'] ?? null; $queueName = $data['Queue'] ?? null;
$this->saveQueues($queueNumber, $tenant); if (!$eventName || !$queueName) {
if (!$eventName || !$queueNumber) {
return response()->json(['status' => 'ignored']); return response()->json(['status' => 'ignored']);
} }
$queue = Queue::where('tenant_id', $tenant->id) $queue = Queue::where('tenant_id', $tenant->id)
->where('source_id', $queueNumber) ->where('source_id', $queueName) // Procura "08000" na coluna source_id
->first();; ->first();;
if (!$queue) { if (!$queue) {
return response()->json(['error' => 'Fila não encontrada'], 404); return response()->json(['error' => 'Fila não encontrada'], 404);
} }
switch ($eventName) { switch ($eventName) {
case 'QueueCallerJoin': case 'QueueCallerJoin':
$this->handleJoin($queue, $data); $this->handleJoin($queue, $data);
@ -63,7 +60,7 @@ public function handle(Request $request)
private function handleJoin($queue, $data) private function handleJoin($queue, $data)
{ {
WaitingList::create([ WaitingList::create([
'tenant_id' => $queue->tenant_id, 'tenant_id' => $queue->tenant_id, // <--- ADICIONE ESTA LINHA
'queue_id' => $queue->id, 'queue_id' => $queue->id,
'caller_number' => $data['CallerIDNum'], 'caller_number' => $data['CallerIDNum'],
'caller_name' => $data['CallerIDName'] ?? 'Desconhecido', 'caller_name' => $data['CallerIDName'] ?? 'Desconhecido',
@ -135,19 +132,9 @@ private function updateMetric($queue, $field, $value)
$metric = $this->getTodayMetric($queue); $metric = $this->getTodayMetric($queue);
$metric->increment($field, $value); $metric->increment($field, $value);
} }
private function saveQueues($queue, $tenant)
{
$existingQueues = Queue::where('source_id', $queue)->exists();
if (!$existingQueues) {
Queue::create(['tenant_id' => $tenant->id, 'type' => 'voice', 'source_id' => $queue]);
exit;
}
}
private function broadcastUpdate($queue) private function broadcastUpdate($queue)
{ {
// Dispara o evento apenas para o Tenant dono da fila
broadcast(new DashboardUpdate($queue->tenant_id)); broadcast(new DashboardUpdate($queue->tenant_id));
} }
} }

View File

@ -1,30 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Queue;
use Illuminate\Support\Facades\Auth;
class QueueController extends Controller
{
public function setQueueName(Request $request)
{
$validated = $request->validate([
'queue_number' => 'required|numeric',
'friendly_name' => 'required|string|max:255'
]);
Queue::updateOrCreate(
[
'tenant_id' => Auth::user()->tenant_id,
'source_id' => $validated['queue_number'],
],
[
'name' => $validated['friendly_name']
]
);
return back()->with('message', 'Nome da fila atualizado com sucesso!');
}
}

View File

@ -21,7 +21,7 @@ public function up()
Schema::create('queues', function (Blueprint $table) { Schema::create('queues', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('tenant_id')->constrained()->cascadeOnDelete(); $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
$table->string('name')->nullable(); $table->string('name');
$table->string('type')->default('voice'); // voice, whatsapp $table->string('type')->default('voice'); // voice, whatsapp
$table->string('source_id')->nullable(); // ID no Asterisk/Helena $table->string('source_id')->nullable(); // ID no Asterisk/Helena
$table->integer('sla_threshold')->default(20); // Meta de SLA em segundos $table->integer('sla_threshold')->default(20); // Meta de SLA em segundos
@ -59,7 +59,7 @@ public function up()
$table->id(); $table->id();
$table->foreignId('queue_id')->constrained()->cascadeOnDelete(); $table->foreignId('queue_id')->constrained()->cascadeOnDelete();
$table->date('date'); $table->date('date');
// Métricas visíveis na imagem // Métricas visíveis na imagem
$table->integer('received_count')->default(0); $table->integer('received_count')->default(0);
$table->integer('answered_count')->default(0); $table->integer('answered_count')->default(0);
@ -69,9 +69,9 @@ public function up()
$table->integer('avg_talk_time')->default(0); // Em segundos $table->integer('avg_talk_time')->default(0); // Em segundos
$table->integer('max_wait_time')->default(0); // Em segundos $table->integer('max_wait_time')->default(0); // Em segundos
$table->integer('transferred_count')->default(0); $table->integer('transferred_count')->default(0);
$table->timestamps(); $table->timestamps();
// Garante uma métrica por fila por dia // Garante uma métrica por fila por dia
$table->unique(['queue_id', 'date']); $table->unique(['queue_id', 'date']);
}); });
@ -97,4 +97,4 @@ public function down()
Schema::dropIfExists('queues'); Schema::dropIfExists('queues');
Schema::dropIfExists('tenants'); Schema::dropIfExists('tenants');
} }
}; };

View File

@ -1,107 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
const showModal = ref(false);
const form = useForm({
queue_number: '',
friendly_name: '',
});
const openModal = () => {
showModal.value = true;
// Pequeno delay para garantir que o elemento foi "teleportado" antes de focar
setTimeout(() => document.getElementById('queue_number')?.focus(), 100);
};
const closeModal = () => {
showModal.value = false;
form.reset();
form.clearErrors();
};
const submitQueueInfo = () => {
form.post(route('queues.store'), {
preserveScroll: true,
onSuccess: () => closeModal(),
});
};
</script>
<template>
<button
type="button"
class="block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out"
@click="openModal"
>
Configurar Filas
</button>
<Teleport to="body">
<div v-if="showModal" class="fixed inset-0 z-50 overflow-y-auto px-4 py-6 sm:px-0 flex items-center justify-center">
<div class="fixed inset-0 transform transition-all" @click="closeModal">
<div class="absolute inset-0 bg-gray-900 opacity-75"></div>
</div>
<div class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full sm:max-w-lg sm:mx-auto z-50">
<div class="px-6 py-4 border-b border-gray-200">
<h3 class="text-lg font-medium text-gray-900">
Vincular Fila ao Nome
</h3>
</div>
<div class="px-6 py-4">
<div class="grid grid-cols-1 gap-4">
<div>
<label for="queue_number" class="block font-medium text-sm text-gray-700">ID da Fila (Asterisk)</label>
<input
id="queue_number"
v-model="form.queue_number"
type="text"
placeholder="Ex: 900"
class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm mt-1 block w-full"
/>
<div v-if="form.errors.queue_number" class="text-red-600 text-sm mt-1">{{ form.errors.queue_number }}</div>
</div>
<div>
<label for="friendly_name" class="block font-medium text-sm text-gray-700">Nome de Exibição</label>
<input
id="friendly_name"
v-model="form.friendly_name"
type="text"
placeholder="Ex: Comercial"
class="border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm mt-1 block w-full"
@keyup.enter="submitQueueInfo"
/>
<div v-if="form.errors.friendly_name" class="text-red-600 text-sm mt-1">{{ form.errors.friendly_name }}</div>
</div>
</div>
</div>
<div class="px-6 py-4 bg-gray-100 text-right flex justify-end gap-2">
<button
type="button"
class="inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 transition ease-in-out duration-150"
@click="closeModal"
>
Cancelar
</button>
<button
type="button"
class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 transition ease-in-out duration-150"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="submitQueueInfo"
>
Salvar
</button>
</div>
</div>
</div>
</Teleport>
</template>

View File

@ -4,15 +4,16 @@ import ApplicationLogo from '@/Components/ApplicationLogo.vue';
import Dropdown from '@/Components/Dropdown.vue'; import Dropdown from '@/Components/Dropdown.vue';
import DropdownLink from '@/Components/DropdownLink.vue'; import DropdownLink from '@/Components/DropdownLink.vue';
import { Link } from '@inertiajs/vue3'; import { Link } from '@inertiajs/vue3';
import SetQueueNameForm from '@/Components/SetQueueNameForm.vue';
const showingSidebar = ref(false); const showingSidebar = ref(false);
const menuItems = [ const menuItems = [
{ name: 'Dashboard', route: 'dashboard', icon: 'M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z' }, { name: 'Dashboard', route: 'dashboard', icon: 'M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z' },
{ name: 'Monitoramento', route: '#', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' }, { name: 'Monitoramento', route: '#', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
{ name: 'Relatórios', route: '#', icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' },
{ name: 'Filas', route: '#', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' }, { name: 'Filas', route: '#', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' },
{ name: 'Agentes', route: '#', icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z' }, { name: 'Agentes', route: '#', icon: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z' },
{ name: 'Configurações', route: '#', icon: 'M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z' },
]; ];
</script> </script>
@ -114,15 +115,12 @@ const menuItems = [
<template #content> <template #content>
<DropdownLink :href="route('profile.edit')"> Perfil </DropdownLink> <DropdownLink :href="route('profile.edit')"> Perfil </DropdownLink>
<DropdownLink :href="route('logout')" method="post" as="button"> Sair </DropdownLink> <DropdownLink :href="route('logout')" method="post" as="button"> Sair </DropdownLink>
<SetQueueNameForm />
</template> </template>
</Dropdown> </Dropdown>
</div> </div>
</div> </div>
</header> </header>
<main class="flex-1 overflow-x-hidden overflow-y-auto"> <main class="flex-1 overflow-x-hidden overflow-y-auto">
<div class="container mx-auto px-6 py-8"> <div class="container mx-auto px-6 py-8">
<slot /> <slot />

View File

@ -1,26 +1,21 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<head> <title inertia>{{ config('app.name', 'Laravel') }}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}"> <!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<title inertia>{{ config('app.name', 'Laravel') }}</title> <link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Fonts --> <!-- Scripts -->
<link rel="preconnect" href="https://fonts.bunny.net"> @routes
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" /> @vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"])
@inertiaHead
<!-- Scripts --> </head>
@routes <body class="font-sans antialiased">
@vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"]) @inertia
@inertiaHead </body>
</head> </html>
<body class="font-sans antialiased">
@inertia
</body>
</html>

View File

@ -12,43 +12,47 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () { Route::middleware('guest')->group(function () {
Route::get('register', [RegisteredUserController::class, 'create'])
->name('register');
Route::post('register', [RegisteredUserController::class, 'store']);
Route::get('login', [AuthenticatedSessionController::class, 'create']) Route::get('login', [AuthenticatedSessionController::class, 'create'])
->name('login'); ->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store']); Route::post('login', [AuthenticatedSessionController::class, 'store']);
// Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
// ->name('password.request'); ->name('password.request');
// Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
// ->name('password.email'); ->name('password.email');
// Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
// ->name('password.reset'); ->name('password.reset');
// Route::post('reset-password', [NewPasswordController::class, 'store']) Route::post('reset-password', [NewPasswordController::class, 'store'])
// ->name('password.store'); ->name('password.store');
}); });
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {
// Route::get('verify-email', EmailVerificationPromptController::class) Route::get('verify-email', EmailVerificationPromptController::class)
// ->name('verification.notice'); ->name('verification.notice');
// Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
// ->middleware(['signed', 'throttle:6,1']) ->middleware(['signed', 'throttle:6,1'])
// ->name('verification.verify'); ->name('verification.verify');
// Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
// ->middleware('throttle:6,1') ->middleware('throttle:6,1')
// ->name('verification.send'); ->name('verification.send');
// Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
// ->name('password.confirm'); ->name('password.confirm');
// Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
// Route::put('password', [PasswordController::class, 'update'])->name('password.update'); Route::put('password', [PasswordController::class, 'update'])->name('password.update');
Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout'); ->name('logout');

View File

@ -5,7 +5,6 @@
use Illuminate\Foundation\Application; use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use Inertia\Inertia; use Inertia\Inertia;
use App\Http\Controllers\QueueController;
Route::get('/', function () { Route::get('/', function () {
return Inertia::render('Welcome', [ return Inertia::render('Welcome', [
@ -16,9 +15,7 @@
]); ]);
}); });
Route::post('/queues', [QueueController::class, 'setQueueName'])->middleware(['auth'])->name('queues.store'); Route::get('/dashboard', [DashboardController::class, 'index'])->middleware(['auth', 'verified'])->name('dashboard');
Route::get('/dashboard', [DashboardController::class, 'index'])->middleware(['auth'])->name('dashboard');
Route::middleware('auth')->group(function () { Route::middleware('auth')->group(function () {
Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');