feat: header con mensajes y notificaciones en tiempo real
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"PowerShell(netstat *)"
|
||||
"PowerShell(netstat *)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit -m ' *)",
|
||||
"Bash(git push *)",
|
||||
"Bash(php artisan *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Events;
|
||||
|
||||
use Illuminate\Broadcasting\Channel;
|
||||
use Illuminate\Broadcasting\InteractsWithSockets;
|
||||
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
|
||||
use Illuminate\Foundation\Events\Dispatchable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
|
||||
class NewNotification implements ShouldBroadcastNow
|
||||
{
|
||||
use Dispatchable, InteractsWithSockets, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public int $userId,
|
||||
public string $title,
|
||||
public string $body,
|
||||
public string $icon = 'fa-bell',
|
||||
public string $color = 'primary',
|
||||
public ?string $url = null,
|
||||
) {}
|
||||
|
||||
public function broadcastOn(): array
|
||||
{
|
||||
return [new Channel('notifications.' . $this->userId)];
|
||||
}
|
||||
|
||||
public function broadcastWith(): array
|
||||
{
|
||||
return [
|
||||
'title' => $this->title,
|
||||
'body' => $this->body,
|
||||
'icon' => $this->icon,
|
||||
'color' => $this->color,
|
||||
'url' => $this->url,
|
||||
'time' => now()->format('H:i'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\View\Composers\HeaderComposer;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Mail\Events\MessageSending;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
public function register(): void {}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
View::composer('layouts._partials.header', HeaderComposer::class);
|
||||
|
||||
Event::listen(MessageSending::class, function ($event) {
|
||||
$event->message->getHeaders()->addTextHeader(
|
||||
'List-Unsubscribe',
|
||||
'<mailto:bajas@tudominio.com>, <https://tudominio.com/unsubscribe>'
|
||||
);
|
||||
$event->message->getHeaders()->addTextHeader(
|
||||
'List-Unsubscribe-Post',
|
||||
'List-Unsubscribe=One-Click'
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Event::listen(MessageSending::class, function ($event) {
|
||||
|
||||
$event->message->getHeaders()->addTextHeader(
|
||||
'List-Unsubscribe',
|
||||
'<mailto:bajas@tudominio.com>, <https://tudominio.com/unsubscribe>'
|
||||
);
|
||||
|
||||
$event->message->getHeaders()->addTextHeader(
|
||||
'List-Unsubscribe-Post',
|
||||
'List-Unsubscribe=One-Click'
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Composers;
|
||||
|
||||
use App\Models\Message;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class HeaderComposer
|
||||
{
|
||||
public function compose(View $view): void
|
||||
{
|
||||
if (!auth()->check()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userId = auth()->id();
|
||||
|
||||
// Últimas conversaciones con mensajes no leídos
|
||||
$unreadConversations = Message::where('receiver_id', $userId)
|
||||
->whereNull('read_at')
|
||||
->with('sender')
|
||||
->orderBy('created_at', 'desc')
|
||||
->get()
|
||||
->unique('user_id')
|
||||
->take(5);
|
||||
|
||||
$unreadMessagesCount = Message::where('receiver_id', $userId)
|
||||
->whereNull('read_at')
|
||||
->count();
|
||||
|
||||
// Notificaciones no leídas
|
||||
$unreadNotifications = auth()->user()
|
||||
->unreadNotifications()
|
||||
->latest()
|
||||
->take(5)
|
||||
->get();
|
||||
|
||||
$unreadNotificationsCount = auth()->user()->unreadNotifications()->count();
|
||||
|
||||
$view->with([
|
||||
'headerUnreadConversations' => $unreadConversations,
|
||||
'headerUnreadMessagesCount' => $unreadMessagesCount,
|
||||
'headerUnreadNotifications' => $unreadNotifications,
|
||||
'headerUnreadNotificationsCount' => $unreadNotificationsCount,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->string('type');
|
||||
$table->morphs('notifiable');
|
||||
$table->text('data');
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
};
|
||||
@@ -4,13 +4,12 @@
|
||||
<nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">
|
||||
|
||||
<!-- Sidebar Toggle (Topbar) -->
|
||||
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3 ">
|
||||
<button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
|
||||
<i class="fa fa-bars"></i>
|
||||
</button>
|
||||
|
||||
<!-- Topbar Search -->
|
||||
<form
|
||||
class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
|
||||
<form class="d-none d-sm-inline-block form-inline mr-auto ml-md-3 my-2 my-md-0 mw-100 navbar-search">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control bg-light border-0 small" placeholder="Buscar..."
|
||||
aria-label="Search" aria-describedby="basic-addon2">
|
||||
@@ -22,22 +21,20 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Topbar Navbar -->
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<!-- Nav Item - Search Dropdown (Visible Only XS) -->
|
||||
<ul class="navbar-nav ml-auto">
|
||||
|
||||
<!-- Nav Item - Search (XS) -->
|
||||
<li class="nav-item dropdown no-arrow d-sm-none">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="searchDropdown" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-search fa-fw"></i>
|
||||
</a>
|
||||
<!-- Dropdown - Messages -->
|
||||
<div class="dropdown-menu dropdown-menu-right p-3 shadow animated--grow-in"
|
||||
aria-labelledby="searchDropdown">
|
||||
<form class="form-inline mr-auto w-100 navbar-search">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control bg-light border-0 small"
|
||||
placeholder="Search for..." aria-label="Search"
|
||||
aria-describedby="basic-addon2">
|
||||
placeholder="Buscar..." aria-label="Search">
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-primary" type="button">
|
||||
<i class="fas fa-search fa-sm"></i>
|
||||
@@ -48,162 +45,192 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Nav Item - Alerts -->
|
||||
<!-- Nav Item - Notificaciones -->
|
||||
<li class="nav-item dropdown no-arrow mx-1">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="alertsDropdown" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-bell fa-fw"></i>
|
||||
<!-- Counter - Alerts -->
|
||||
<span class="badge badge-danger badge-counter">3+</span>
|
||||
@if($headerUnreadNotificationsCount > 0)
|
||||
<span class="badge badge-danger badge-counter" id="notif-badge">
|
||||
{{ $headerUnreadNotificationsCount > 9 ? '9+' : $headerUnreadNotificationsCount }}
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-danger badge-counter d-none" id="notif-badge">0</span>
|
||||
@endif
|
||||
</a>
|
||||
<!-- Dropdown - Alerts -->
|
||||
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
|
||||
aria-labelledby="alertsDropdown">
|
||||
<h6 class="dropdown-header">
|
||||
Alerts Center
|
||||
</h6>
|
||||
<a class="dropdown-item d-flex align-items-center" href="#">
|
||||
<div class="mr-3">
|
||||
<div class="icon-circle bg-primary">
|
||||
<i class="fas fa-file-alt text-white"></i>
|
||||
<h6 class="dropdown-header">Notificaciones</h6>
|
||||
|
||||
@forelse($headerUnreadNotifications as $notif)
|
||||
@php $data = $notif->data; @endphp
|
||||
<a class="dropdown-item d-flex align-items-center"
|
||||
href="{{ $data['url'] ?? '#' }}"
|
||||
onclick="markNotifRead('{{ $notif->id }}')">
|
||||
<div class="mr-3">
|
||||
<div class="icon-circle bg-{{ $data['color'] ?? 'primary' }}">
|
||||
<i class="fas {{ $data['icon'] ?? 'fa-bell' }} text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="small text-gray-500">December 12, 2019</div>
|
||||
<span class="font-weight-bold">A new monthly report is ready to download!</span>
|
||||
</div>
|
||||
</a>
|
||||
<a class="dropdown-item d-flex align-items-center" href="#">
|
||||
<div class="mr-3">
|
||||
<div class="icon-circle bg-success">
|
||||
<i class="fas fa-donate text-white"></i>
|
||||
<div>
|
||||
<div class="small text-gray-500">{{ $notif->created_at->diffForHumans() }}</div>
|
||||
<span class="font-weight-bold">{{ $data['title'] ?? '' }}</span>
|
||||
@if(!empty($data['body']))
|
||||
<div class="small text-truncate" style="max-width:200px">{{ $data['body'] }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="dropdown-item text-center text-muted py-3">
|
||||
<i class="fas fa-check-circle text-success mr-1"></i> Sin notificaciones nuevas
|
||||
</div>
|
||||
<div>
|
||||
<div class="small text-gray-500">December 7, 2019</div>
|
||||
$290.29 has been deposited into your account!
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
<a class="dropdown-item text-center small text-gray-500"
|
||||
href="#" onclick="markAllNotifsRead(); return false;">
|
||||
Marcar todas como leídas
|
||||
</a>
|
||||
<a class="dropdown-item d-flex align-items-center" href="#">
|
||||
<div class="mr-3">
|
||||
<div class="icon-circle bg-warning">
|
||||
<i class="fas fa-exclamation-triangle text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="small text-gray-500">December 2, 2019</div>
|
||||
Spending Alert: We've noticed unusually high spending for your account.
|
||||
</div>
|
||||
</a>
|
||||
<a class="dropdown-item text-center small text-gray-500" href="#">Show All Alerts</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- Nav Item - Messages -->
|
||||
<!-- Nav Item - Mensajes -->
|
||||
<li class="nav-item dropdown no-arrow mx-1">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="messagesDropdown" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<a class="nav-link dropdown-toggle" href="{{ route('chat.index') }}" id="messagesDropdown"
|
||||
role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i class="fas fa-envelope fa-fw"></i>
|
||||
<!-- Counter - Messages -->
|
||||
<span class="badge badge-danger badge-counter">7</span>
|
||||
@if($headerUnreadMessagesCount > 0)
|
||||
<span class="badge badge-danger badge-counter" id="msg-badge">
|
||||
{{ $headerUnreadMessagesCount > 9 ? '9+' : $headerUnreadMessagesCount }}
|
||||
</span>
|
||||
@else
|
||||
<span class="badge badge-danger badge-counter d-none" id="msg-badge">0</span>
|
||||
@endif
|
||||
</a>
|
||||
<!-- Dropdown - Messages -->
|
||||
<div class="dropdown-list dropdown-menu dropdown-menu-right shadow animated--grow-in"
|
||||
aria-labelledby="messagesDropdown">
|
||||
<h6 class="dropdown-header">
|
||||
Message Center
|
||||
</h6>
|
||||
<a class="dropdown-item d-flex align-items-center" href="#">
|
||||
<div class="dropdown-list-image mr-3">
|
||||
<img class="rounded-circle" src="img/undraw_profile_1.svg"
|
||||
alt="...">
|
||||
<div class="status-indicator bg-success"></div>
|
||||
</div>
|
||||
<div class="font-weight-bold">
|
||||
<div class="text-truncate">Hi there! I am wondering if you can help me with a
|
||||
problem I've been having.</div>
|
||||
<div class="small text-gray-500">Emily Fowler · 58m</div>
|
||||
<h6 class="dropdown-header">Mensajes</h6>
|
||||
|
||||
@forelse($headerUnreadConversations as $msg)
|
||||
<a class="dropdown-item d-flex align-items-center"
|
||||
href="{{ route('chat.show', $msg->sender) }}">
|
||||
<div class="dropdown-list-image mr-3">
|
||||
<img class="rounded-circle" width="40" height="40"
|
||||
style="object-fit:cover"
|
||||
src="{{ $msg->sender->profile_photo_url }}" alt="">
|
||||
<div class="status-indicator bg-success"></div>
|
||||
</div>
|
||||
<div class="font-weight-bold overflow-hidden">
|
||||
<div class="text-truncate">{{ $msg->body ?? '📎 Archivo adjunto' }}</div>
|
||||
<div class="small text-gray-500">
|
||||
{{ $msg->sender->name }} · {{ $msg->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
@empty
|
||||
<div class="dropdown-item text-center text-muted py-3">
|
||||
<i class="fas fa-check-circle text-success mr-1"></i> Sin mensajes nuevos
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
<a class="dropdown-item text-center small text-gray-500" href="{{ route('chat.index') }}">
|
||||
Ver todos los mensajes
|
||||
</a>
|
||||
<a class="dropdown-item d-flex align-items-center" href="#">
|
||||
<div class="dropdown-list-image mr-3">
|
||||
<img class="rounded-circle" src="img/undraw_profile_2.svg"
|
||||
alt="...">
|
||||
<div class="status-indicator"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-truncate">I have the photos that you ordered last month, how
|
||||
would you like them sent to you?</div>
|
||||
<div class="small text-gray-500">Jae Chun · 1d</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="dropdown-item d-flex align-items-center" href="#">
|
||||
<div class="dropdown-list-image mr-3">
|
||||
<img class="rounded-circle" src="img/undraw_profile_3.svg"
|
||||
alt="...">
|
||||
<div class="status-indicator bg-warning"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-truncate">Last month's report looks great, I am very happy with
|
||||
the progress so far, keep up the good work!</div>
|
||||
<div class="small text-gray-500">Morgan Alvarez · 2d</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="dropdown-item d-flex align-items-center" href="#">
|
||||
<div class="dropdown-list-image mr-3">
|
||||
<img class="rounded-circle" src="img/undraw_profile_3.svg"
|
||||
alt="...">
|
||||
<div class="status-indicator bg-success"></div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-truncate">Am I a good boy? The reason I ask is because someone
|
||||
told me that people say this to all dogs, even if they aren't good...</div>
|
||||
<div class="small text-gray-500">Chicken the Dog · 2w</div>
|
||||
</div>
|
||||
</a>
|
||||
<a class="dropdown-item text-center small text-gray-500" href="#">Read More Messages</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<div class="topbar-divider d-none d-sm-block"></div>
|
||||
|
||||
<!-- Nav Item - User Information -->
|
||||
<!-- Nav Item - Usuario -->
|
||||
<li class="nav-item dropdown no-arrow">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button"
|
||||
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="mr-2 d-none d-lg-inline text-gray-600 small">{{ Auth::user()->name }}</span>
|
||||
<img class="img-profile rounded-circle"
|
||||
src="img/undraw_profile.svg">
|
||||
<img class="img-profile rounded-circle" src="{{ Auth::user()->profile_photo_url }}">
|
||||
</a>
|
||||
<!-- Dropdown - User Information -->
|
||||
<div class="dropdown-menu dropdown-menu-right shadow animated--grow-in"
|
||||
aria-labelledby="userDropdown">
|
||||
|
||||
<a class="dropdown-item" href="{{ route('perfil') }}">
|
||||
<i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i>
|
||||
Mi perfil
|
||||
</a>
|
||||
|
||||
<a class="dropdown-item" href="#">
|
||||
<i class="fas fa-cogs fa-sm fa-fw mr-2 text-gray-400"></i>
|
||||
Settings
|
||||
</a>
|
||||
<a class="dropdown-item" href="#">
|
||||
<i class="fas fa-list fa-sm fa-fw mr-2 text-gray-400"></i>
|
||||
Activity Log
|
||||
<i class="fas fa-user fa-sm fa-fw mr-2 text-gray-400"></i> Mi perfil
|
||||
</a>
|
||||
<div class="dropdown-divider"></div>
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display:none">
|
||||
@csrf
|
||||
</form>
|
||||
<a class="dropdown-item" href="{{ route('logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">
|
||||
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2" style="color:rgb(252, 32, 32);"></i>
|
||||
<span>Salir</span>
|
||||
<a class="dropdown-item" href="{{ route('logout') }}"
|
||||
onclick="event.preventDefault(); document.getElementById('logout-form').submit();">
|
||||
<i class="fas fa-sign-out-alt fa-sm fa-fw mr-2" style="color:rgb(252,32,32)"></i>
|
||||
Salir
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<script>
|
||||
// ── Mensajes en tiempo real ──────────────────────────────
|
||||
function updateBadge(id, increment) {
|
||||
const badge = document.getElementById(id);
|
||||
if (!badge) return;
|
||||
const current = parseInt(badge.textContent) || 0;
|
||||
const next = current + increment;
|
||||
if (next <= 0) {
|
||||
badge.classList.add('d-none');
|
||||
badge.textContent = '0';
|
||||
} else {
|
||||
badge.classList.remove('d-none');
|
||||
badge.textContent = next > 9 ? '9+' : next;
|
||||
}
|
||||
}
|
||||
|
||||
function waitForEcho(cb) {
|
||||
if (window.Echo) cb(); else setTimeout(() => waitForEcho(cb), 100);
|
||||
}
|
||||
|
||||
waitForEcho(() => {
|
||||
const authId = {{ auth()->id() }};
|
||||
|
||||
// Badge de mensajes
|
||||
window.Echo.channel(`chat.${authId}`)
|
||||
.listen('MessageSent', () => updateBadge('msg-badge', 1));
|
||||
|
||||
// Badge de notificaciones
|
||||
window.Echo.channel(`notifications.${authId}`)
|
||||
.listen('NewNotification', (e) => {
|
||||
updateBadge('notif-badge', 1);
|
||||
// Agregar al dropdown sin recargar
|
||||
const list = document.querySelector('#alertsDropdown + .dropdown-list');
|
||||
const empty = list.querySelector('.text-muted.py-3');
|
||||
if (empty) empty.remove();
|
||||
const item = document.createElement('a');
|
||||
item.className = 'dropdown-item d-flex align-items-center';
|
||||
item.href = e.url ?? '#';
|
||||
item.innerHTML = `
|
||||
<div class="mr-3">
|
||||
<div class="icon-circle bg-${e.color}">
|
||||
<i class="fas ${e.icon} text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="small text-gray-500">${e.time}</div>
|
||||
<span class="font-weight-bold">${e.title}</span>
|
||||
${e.body ? `<div class="small text-truncate" style="max-width:200px">${e.body}</div>` : ''}
|
||||
</div>`;
|
||||
list.querySelector('.dropdown-header').insertAdjacentElement('afterend', item);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Marcar notificaciones como leídas ───────────────────
|
||||
function markNotifRead(id) {
|
||||
axios.post('/notifications/' + id + '/read').catch(() => {});
|
||||
}
|
||||
|
||||
function markAllNotifsRead() {
|
||||
axios.post('/notifications/read-all').then(() => {
|
||||
document.getElementById('notif-badge').classList.add('d-none');
|
||||
document.querySelectorAll('#alertsDropdown + .dropdown-list .dropdown-item[onclick]')
|
||||
.forEach(el => el.classList.remove('font-weight-bold'));
|
||||
}).catch(() => {});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -9,3 +9,7 @@ Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
|
||||
Broadcast::channel('chat.{receiverId}', function ($user, $receiverId) {
|
||||
return (int) $user->id === (int) $receiverId;
|
||||
});
|
||||
|
||||
Broadcast::channel('notifications.{userId}', function ($user, $userId) {
|
||||
return (int) $user->id === (int) $userId;
|
||||
});
|
||||
|
||||
+3
-1
@@ -56,7 +56,9 @@ Route::middleware(['auth:sanctum',config('jetstream.auth_session'),'verified',
|
||||
Route::get('/chat', [ChatController::class, 'index'])->name('chat.index');
|
||||
Route::get('/chat/{user}', [ChatController::class, 'show'])->name('chat.show');
|
||||
Route::post('/chat/send', [ChatController::class, 'send'])->name('chat.send');
|
||||
Route::get('/files/{path}', [FileController::class, 'serve'])->where('path', '.*')->name('files.serve');Route::resource('/docente',DocenteController::class);
|
||||
Route::get('/files/{path}', [FileController::class, 'serve'])->where('path', '.*')->name('files.serve');
|
||||
Route::post('/notifications/{id}/read', fn($id) => auth()->user()->notifications()->findOrFail($id)->markAsRead())->name('notifications.read');
|
||||
Route::post('/notifications/read-all', fn() => auth()->user()->unreadNotifications->markAsRead())->name('notifications.read-all');Route::resource('/docente',DocenteController::class);
|
||||
Route::resource('/documento',DocumentoController::class);
|
||||
Route::get('/documento/ver/{id}', function ($id) { $doc = Documento::findOrFail($id); $alumno = auth()->user()->alumnos()->first(); if (!$alumno || $doc->alumno_id !== $alumno->id) { abort(403); } return response()->file( storage_path('app/public/'.$doc->archivo) ); });
|
||||
Route::get('/documento/ver/{id}', [DocumentoController::class, 'ver'])->name('documento.ver');
|
||||
|
||||
Reference in New Issue
Block a user