Compare commits

..

2 Commits

Author SHA1 Message Date
lukidev ae26402490
feat: Cria filas ao receber requisição | Nomes amigáveis para filas | Componente para definir nome da fila.
feat: Cria filas ao receber requisição | Nomes amigáveis para filas | Componente para definir nome da fila.
2025-12-16 17:35:39 -03:00
LukiBeg 90637ace07 feat: Cria filas ao receber requisição | Nomes amigáveis para filas | Componente para definir nome da fila. 2025-12-16 17:33:50 -03:00
8 changed files with 209 additions and 53 deletions

View File

@ -24,20 +24,23 @@ public function handle(Request $request)
$data = $request->all(); $data = $request->all();
$eventName = $data['Event'] ?? null; $eventName = $data['Event'] ?? null;
$queueName = $data['Queue'] ?? null; $queueNumber = $data['Queue'] ?? null;
if (!$eventName || !$queueName) { $this->saveQueues($queueNumber, $tenant);
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', $queueName) // Procura "08000" na coluna source_id ->where('source_id', $queueNumber)
->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);
@ -60,7 +63,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, // <--- ADICIONE ESTA LINHA 'tenant_id' => $queue->tenant_id,
'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',
@ -132,9 +135,19 @@ 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

@ -0,0 +1,30 @@
<?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'); $table->string('name')->nullable();
$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

View File

@ -0,0 +1,107 @@
<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,16 +4,15 @@ 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>
@ -115,12 +114,15 @@ 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,9 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}"> <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title inertia>{{ config('app.name', 'Laravel') }}</title> <title inertia>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts --> <!-- Fonts -->
@ -15,7 +18,9 @@
@vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"]) @vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"])
@inertiaHead @inertiaHead
</head> </head>
<body class="font-sans antialiased"> <body class="font-sans antialiased">
@inertia @inertia
</body> </body>
</html> </html>

View File

@ -12,47 +12,43 @@
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,6 +5,7 @@
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', [
@ -15,7 +16,9 @@
]); ]);
}); });
Route::get('/dashboard', [DashboardController::class, 'index'])->middleware(['auth', 'verified'])->name('dashboard'); Route::post('/queues', [QueueController::class, 'setQueueName'])->middleware(['auth'])->name('queues.store');
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');