mirror of https://github.com/Lukibeg/OmniBoard.git
feat|fix: Timezone em America/Sao_Paulo|Laravel Reverb + Pusher + Echo|Atualizações visuais.
This commit is contained in:
parent
4f24b18dd5
commit
80a4731cd0
25
.env.example
25
.env.example
|
|
@ -24,8 +24,15 @@ DB_CONNECTION=mysql
|
|||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=omniboard
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
DB_USERNAME=omniboard_user
|
||||
DB_PASSWORD="*Ingline.Sys#9420%SECURITY#"
|
||||
|
||||
REVERB_APP_ID=test
|
||||
REVERB_APP_KEY=test
|
||||
REVERB_APP_SECRET=test
|
||||
REVERB_HOST="localhost"
|
||||
REVERB_PORT=8081
|
||||
REVERB_SCHEME=http
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
|
|
@ -63,3 +70,17 @@ AWS_BUCKET=
|
|||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
BROADCAST_CONNECTION=reverb
|
||||
|
||||
REVERB_APP_ID=348643
|
||||
REVERB_APP_KEY=v40nrhrty4rxpsv9mnt1
|
||||
REVERB_APP_SECRET=vf0mesdcuze3dob3nage
|
||||
REVERB_HOST="localhost"
|
||||
REVERB_PORT=8081
|
||||
REVERB_SCHEME=http
|
||||
|
||||
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
||||
VITE_REVERB_HOST="${REVERB_HOST}"
|
||||
VITE_REVERB_PORT="${REVERB_PORT}"
|
||||
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Broadcasting\PrivateChannel; // Importante para segurança
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class DashboardUpdate implements ShouldBroadcast
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public $tenantId;
|
||||
|
||||
// Recebemos o ID da empresa que sofreu alteração
|
||||
public function __construct($tenantId)
|
||||
{
|
||||
$this->tenantId = $tenantId;
|
||||
}
|
||||
|
||||
// Define o canal privado: "dashboard.{tenant_id}"
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [
|
||||
new PrivateChannel('dashboard.' . $this->tenantId),
|
||||
];
|
||||
}
|
||||
|
||||
// Opcional: Nome do evento no Front (padrão é o nome da classe)
|
||||
public function broadcastAs()
|
||||
{
|
||||
return 'metrics.updated';
|
||||
}
|
||||
}
|
||||
|
|
@ -9,12 +9,12 @@
|
|||
use App\Models\WaitingList;
|
||||
use App\Models\DailyMetric;
|
||||
use Carbon\Carbon;
|
||||
use App\Events\DashboardUpdate;
|
||||
|
||||
class AmiEventController extends Controller
|
||||
{
|
||||
public function handle(Request $request)
|
||||
{
|
||||
// 1. Autenticação via Token (Header: X-Tenant-Token)
|
||||
$token = $request->header('X-Tenant-Token');
|
||||
$tenant = Tenant::where('api_key', $token)->first();
|
||||
|
||||
|
|
@ -22,7 +22,6 @@ public function handle(Request $request)
|
|||
return response()->json(['error' => 'Unauthorized'], 401);
|
||||
}
|
||||
|
||||
// 2. Dados do Evento
|
||||
$data = $request->all();
|
||||
$eventName = $data['Event'] ?? null;
|
||||
$queueName = $data['Queue'] ?? null;
|
||||
|
|
@ -31,17 +30,14 @@ public function handle(Request $request)
|
|||
return response()->json(['status' => 'ignored']);
|
||||
}
|
||||
|
||||
// DEPOIS (Correto)
|
||||
$queue = Queue::where('tenant_id', $tenant->id)
|
||||
->where('source_id', $queueName) // Procura "08000" na coluna source_id
|
||||
->first();;
|
||||
|
||||
// Se a fila não existe no banco, ignoramos ou criamos automaticamente (opcional)
|
||||
if (!$queue) {
|
||||
return response()->json(['error' => 'Fila não encontrada'], 404);
|
||||
}
|
||||
|
||||
// 4. Processar Lógica de Negócio
|
||||
switch ($eventName) {
|
||||
case 'QueueCallerJoin':
|
||||
$this->handleJoin($queue, $data);
|
||||
|
|
@ -60,11 +56,9 @@ public function handle(Request $request)
|
|||
return response()->json(['status' => 'success']);
|
||||
}
|
||||
|
||||
// LÓGICA DE NEGÓCIO (A mesma do script anterior, mas agora com Eloquent)
|
||||
|
||||
private function handleJoin($queue, $data)
|
||||
{
|
||||
// Adiciona na Lista de Espera COM TENANT_ID
|
||||
WaitingList::create([
|
||||
'tenant_id' => $queue->tenant_id, // <--- ADICIONE ESTA LINHA
|
||||
'queue_id' => $queue->id,
|
||||
|
|
@ -73,28 +67,25 @@ private function handleJoin($queue, $data)
|
|||
'entered_at' => now(),
|
||||
]);
|
||||
|
||||
// Incrementa Total do Dia
|
||||
$this->updateMetric($queue, 'received_count', 1);
|
||||
$this->broadcastUpdate($queue);
|
||||
}
|
||||
|
||||
private function handleConnect($queue, $data)
|
||||
{
|
||||
// Remove da Lista de Espera
|
||||
WaitingList::where('queue_id', $queue->id)
|
||||
->where('caller_number', $data['CallerIDNum'])
|
||||
->delete();
|
||||
|
||||
// Atualiza TME (Média Ponderada)
|
||||
// Nota: Isso é simplificado. Em produção real, calcular média ponderada em SQL é melhor.
|
||||
$metric = $this->getTodayMetric($queue);
|
||||
$holdTime = intval($data['HoldTime'] ?? 0);
|
||||
|
||||
// Novo TME = ((Atual * Qtd) + Novo) / (Qtd + 1)
|
||||
$newAvg = (($metric->avg_wait_time * $metric->answered_count) + $holdTime) / ($metric->answered_count + 1);
|
||||
|
||||
$metric->avg_wait_time = $newAvg;
|
||||
$metric->answered_count += 1;
|
||||
$metric->save();
|
||||
$this->broadcastUpdate($queue);
|
||||
}
|
||||
|
||||
private function handleComplete($queue, $data)
|
||||
|
|
@ -102,28 +93,24 @@ private function handleComplete($queue, $data)
|
|||
$talkTime = intval($data['TalkTime'] ?? 0);
|
||||
$metric = $this->getTodayMetric($queue);
|
||||
|
||||
// Evita divisão por zero se o connect falhou
|
||||
if ($metric->answered_count > 0) {
|
||||
// Novo TMA = ((Atual * (Qtd-1)) + Novo) / Qtd
|
||||
// Nota: Usamos Qtd (answered_count) como divisor pois a chamada já foi contada no Connect
|
||||
$newAvg = (($metric->avg_talk_time * ($metric->answered_count - 1)) + $talkTime) / $metric->answered_count;
|
||||
$metric->avg_talk_time = $newAvg;
|
||||
$metric->save();
|
||||
}
|
||||
$this->broadcastUpdate($queue);
|
||||
}
|
||||
|
||||
private function handleAbandon($queue, $data)
|
||||
{
|
||||
// Remove da Espera
|
||||
WaitingList::where('queue_id', $queue->id)
|
||||
->where('caller_number', $data['CallerIDNum'])
|
||||
->delete();
|
||||
|
||||
// Incrementa Abandonos
|
||||
$this->updateMetric($queue, 'abandoned_count', 1);
|
||||
$this->broadcastUpdate($queue);
|
||||
}
|
||||
|
||||
// Helpers
|
||||
private function getTodayMetric($queue)
|
||||
{
|
||||
return DailyMetric::firstOrCreate(
|
||||
|
|
@ -132,7 +119,7 @@ private function getTodayMetric($queue)
|
|||
'date' => Carbon::today()
|
||||
],
|
||||
[
|
||||
'tenant_id' => $queue->tenant_id, // <--- O PULO DO GATO: Herda o ID da Fila
|
||||
'tenant_id' => $queue->tenant_id,
|
||||
'received_count' => 0,
|
||||
'answered_count' => 0,
|
||||
'abandoned_count' => 0
|
||||
|
|
@ -145,4 +132,9 @@ private function updateMetric($queue, $field, $value)
|
|||
$metric = $this->getTodayMetric($queue);
|
||||
$metric->increment($field, $value);
|
||||
}
|
||||
private function broadcastUpdate($queue)
|
||||
{
|
||||
// Dispara o evento apenas para o Tenant dono da fila
|
||||
broadcast(new DashboardUpdate($queue->tenant_id));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@
|
|||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,12 +1,57 @@
|
|||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
import { Head, router, usePage } from '@inertiajs/vue3';
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
|
||||
defineProps({
|
||||
const props = defineProps({
|
||||
queues: Array
|
||||
});
|
||||
|
||||
// Formata segundos para MM:SS
|
||||
const page = usePage();
|
||||
const userTenantId = page.props.auth.user.tenant_id;
|
||||
|
||||
// Variável para guardar a inscrição do canal
|
||||
let channel = null;
|
||||
|
||||
onMounted(() => {
|
||||
if (userTenantId) {
|
||||
// Verifica se o Echo já está carregado
|
||||
if (window.Echo) {
|
||||
connectToChannel();
|
||||
} else {
|
||||
// Se não, espera um pouco e tenta de novo (Fallback simples)
|
||||
// Ou melhor: escuta o evento de carregamento, mas o setInterval é mais prático aqui
|
||||
const checkEcho = setInterval(() => {
|
||||
if (window.Echo) {
|
||||
clearInterval(checkEcho);
|
||||
connectToChannel();
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const connectToChannel = () => {
|
||||
console.log(`📡 Conectando ao canal dashboard.${userTenantId}...`);
|
||||
|
||||
channel = window.Echo.private(`dashboard.${userTenantId}`)
|
||||
.listen('.metrics.updated', (e) => {
|
||||
console.log("🔔 Evento recebido!", e);
|
||||
router.reload({ only: ['queues'], preserveScroll: true });
|
||||
})
|
||||
.error((err) => {
|
||||
console.error("❌ Erro de Conexão/Auth:", err);
|
||||
// Dica: Se o erro for detalhado, ele vai aparecer aqui.
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
if (userTenantId && window.Echo) {
|
||||
console.log(`🔌 Desconectando do canal dashboard.${userTenantId}`);
|
||||
window.Echo.leave(`dashboard.${userTenantId}`);
|
||||
}
|
||||
});
|
||||
|
||||
const formatTime = (seconds) => {
|
||||
if (!seconds && seconds !== 0) return '-';
|
||||
const m = Math.floor(seconds / 60).toString().padStart(2, '0');
|
||||
|
|
@ -14,7 +59,6 @@ const formatTime = (seconds) => {
|
|||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
// Calcula a percentagem com 1 casa decimal
|
||||
const calculatePercentage = (part, total) => {
|
||||
if (!total || total === 0) return '0.0%';
|
||||
const percent = (part / total) * 100;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ defineProps({
|
|||
<h2
|
||||
class="text-xl font-semibold leading-tight text-gray-800"
|
||||
>
|
||||
Profile
|
||||
Meu perfil
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -42,64 +42,46 @@ const closeModal = () => {
|
|||
<section class="space-y-6">
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900">
|
||||
Delete Account
|
||||
Excluir conta
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Once your account is deleted, all of its resources and data will
|
||||
be permanently deleted. Before deleting your account, please
|
||||
download any data or information that you wish to retain.
|
||||
Se você optar por excluir sua conta, todas as informações serão perdidas.
|
||||
Será necessário entrar em contato com a Ingline Systems para gerar uma nova.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<DangerButton @click="confirmUserDeletion">Delete Account</DangerButton>
|
||||
<DangerButton @click="confirmUserDeletion">Deletar conta</DangerButton>
|
||||
|
||||
<Modal :show="confirmingUserDeletion" @close="closeModal">
|
||||
<div class="p-6">
|
||||
<h2
|
||||
class="text-lg font-medium text-gray-900"
|
||||
>
|
||||
Are you sure you want to delete your account?
|
||||
<h2 class="text-lg font-medium text-gray-900">
|
||||
Você tem certeza que deseja deletar sua conta?
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Once your account is deleted, all of its resources and data
|
||||
will be permanently deleted. Please enter your password to
|
||||
confirm you would like to permanently delete your account.
|
||||
Após a exclusão da sua conta, todos os seus recursos e dados
|
||||
serão apagados permanentemente. Digite sua senha para
|
||||
confirmar que deseja excluir sua conta permanentemente.
|
||||
</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<InputLabel
|
||||
for="password"
|
||||
value="Password"
|
||||
class="sr-only"
|
||||
/>
|
||||
<InputLabel for="password" value="Password" class="sr-only" />
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="Password"
|
||||
@keyup.enter="deleteUser"
|
||||
/>
|
||||
<TextInput id="password" ref="passwordInput" v-model="form.password" type="password"
|
||||
class="mt-1 block w-3/4" placeholder="Digite sua senha" @keyup.enter="deleteUser" />
|
||||
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<SecondaryButton @click="closeModal">
|
||||
Cancel
|
||||
Cancelar
|
||||
</SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ms-3"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
@click="deleteUser"
|
||||
>
|
||||
Delete Account
|
||||
<DangerButton class="ms-3" :class="{ 'opacity-25': form.processing }" :disabled="form.processing"
|
||||
@click="deleteUser">
|
||||
Deletar conta
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -37,83 +37,50 @@ const updatePassword = () => {
|
|||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900">
|
||||
Update Password
|
||||
Atualizar senha
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Ensure your account is using a long, random password to stay
|
||||
secure.
|
||||
Certifique-se de que sua conta esteja usando uma senha longa e aleatória para permanecer
|
||||
segura.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form @submit.prevent="updatePassword" class="mt-6 space-y-6">
|
||||
<div>
|
||||
<InputLabel for="current_password" value="Current Password" />
|
||||
<InputLabel for="current_password" value="Credencial atual" />
|
||||
|
||||
<TextInput
|
||||
id="current_password"
|
||||
ref="currentPasswordInput"
|
||||
v-model="form.current_password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
<TextInput id="current_password" ref="currentPasswordInput" v-model="form.current_password"
|
||||
type="password" class="mt-1 block w-full" autocomplete="current-password" />
|
||||
|
||||
<InputError
|
||||
:message="form.errors.current_password"
|
||||
class="mt-2"
|
||||
/>
|
||||
<InputError :message="form.errors.current_password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="password" value="New Password" />
|
||||
<InputLabel for="password" value="Nova credencial" />
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<TextInput id="password" ref="passwordInput" v-model="form.password" type="password"
|
||||
class="mt-1 block w-full" autocomplete="new-password" />
|
||||
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel
|
||||
for="password_confirmation"
|
||||
value="Confirm Password"
|
||||
/>
|
||||
<InputLabel for="password_confirmation" value="Confirme sua credencial nova" />
|
||||
|
||||
<TextInput
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
<TextInput id="password_confirmation" v-model="form.password_confirmation" type="password"
|
||||
class="mt-1 block w-full" autocomplete="new-password" />
|
||||
|
||||
<InputError
|
||||
:message="form.errors.password_confirmation"
|
||||
class="mt-2"
|
||||
/>
|
||||
<InputError :message="form.errors.password_confirmation" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Save</PrimaryButton>
|
||||
<PrimaryButton :disabled="form.processing">Salvar</PrimaryButton>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
leave-active-class="transition ease-in-out"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<p
|
||||
v-if="form.recentlySuccessful"
|
||||
class="text-sm text-gray-600"
|
||||
>
|
||||
Saved.
|
||||
<Transition enter-active-class="transition ease-in-out" enter-from-class="opacity-0"
|
||||
leave-active-class="transition ease-in-out" leave-to-class="opacity-0">
|
||||
<p v-if="form.recentlySuccessful" class="text-sm text-gray-600">
|
||||
Alteração concluída!
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,30 +26,20 @@ const form = useForm({
|
|||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900">
|
||||
Profile Information
|
||||
Informação de perfil
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Update your account's profile information and email address.
|
||||
Atualize as informações do seu perfil e o seu endereço de e-mail.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form
|
||||
@submit.prevent="form.patch(route('profile.update'))"
|
||||
class="mt-6 space-y-6"
|
||||
>
|
||||
<form @submit.prevent="form.patch(route('profile.update'))" class="mt-6 space-y-6">
|
||||
<div>
|
||||
<InputLabel for="name" value="Name" />
|
||||
<InputLabel for="name" value="Nome" />
|
||||
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.name"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
/>
|
||||
<TextInput id="name" type="text" class="mt-1 block w-full" v-model="form.name" required autofocus
|
||||
autocomplete="name" />
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
|
@ -57,53 +47,33 @@ const form = useForm({
|
|||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.email"
|
||||
required
|
||||
autocomplete="username"
|
||||
/>
|
||||
<TextInput id="email" type="email" class="mt-1 block w-full" v-model="form.email" required
|
||||
autocomplete="username" />
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div v-if="mustVerifyEmail && user.email_verified_at === null">
|
||||
<p class="mt-2 text-sm text-gray-800">
|
||||
Your email address is unverified.
|
||||
<Link
|
||||
:href="route('verification.send')"
|
||||
method="post"
|
||||
as="button"
|
||||
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||
>
|
||||
Click here to re-send the verification email.
|
||||
Seu e-mail não está verificado.
|
||||
<Link :href="route('verification.send')" method="post" as="button"
|
||||
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
|
||||
Clique aqui para reenviar sua verificação de e-mail.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-show="status === 'verification-link-sent'"
|
||||
class="mt-2 text-sm font-medium text-green-600"
|
||||
>
|
||||
A new verification link has been sent to your email address.
|
||||
<div v-show="status === 'verification-link-sent'" class="mt-2 text-sm font-medium text-green-600">
|
||||
Um novo link de verificação foi enviado com sucesso para seu e-mail.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Save</PrimaryButton>
|
||||
<PrimaryButton :disabled="form.processing">Salvar</PrimaryButton>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
leave-active-class="transition ease-in-out"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<p
|
||||
v-if="form.recentlySuccessful"
|
||||
class="text-sm text-gray-600"
|
||||
>
|
||||
Saved.
|
||||
<Transition enter-active-class="transition ease-in-out" enter-from-class="opacity-0"
|
||||
leave-active-class="transition ease-in-out" leave-to-class="opacity-0">
|
||||
<p v-if="form.recentlySuccessful" class="text-sm text-gray-600">
|
||||
Alteração concluída!
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,3 +2,18 @@ import axios from 'axios';
|
|||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
|
||||
import Echo from 'laravel-echo';
|
||||
|
||||
import Pusher from 'pusher-js';
|
||||
window.Pusher = Pusher;
|
||||
|
||||
window.Echo = new Echo({
|
||||
broadcaster: 'reverb',
|
||||
key: import.meta.env.VITE_REVERB_APP_KEY,
|
||||
wsHost: import.meta.env.VITE_REVERB_HOST,
|
||||
wsPort: import.meta.env.VITE_REVERB_PORT ?? 80,
|
||||
wssPort: import.meta.env.VITE_REVERB_PORT ?? 443,
|
||||
forceTLS: (import.meta.env.VITE_REVERB_SCHEME ?? 'https') === 'https',
|
||||
enabledTransports: ['ws', 'wss'],
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
|
||||
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
|
||||
return (int) $user->id === (int) $id;
|
||||
Broadcast::channel('dashboard.{tenantId}', function ($user, $tenantId) {
|
||||
// Retorna true se o usuário pertencer ao Tenant que está tentando ouvir
|
||||
return (int) $user->tenant_id === (int) $tenantId;
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue