Notificações
Notificações in-app e por email.
Notificações — MadNotify
Sistema de notificações in-app nativo do MAD: sino no header, central "ver todas", contagem de não lidas e push em tempo real via Laravel Reverb (WebSocket), com degradação automática para polling quando o Reverb está desligado ou indisponível.
Como funciona
MadNotify::to($userId)->subject(...)->action(...)->send()
└── grava 1 linha em mad_comm_notification (conexão `comm`)
└── dispara MadNotificationReceived (Reverb) se config('mad.notifications.reverb')
├── Reverb ligado e saudável → sino atualiza na hora (badge + toast)
└── Reverb desligado/indisponível (try/catch) → só o polling do header
atualiza o sino depois
A persistência nunca depende do Reverb estar no ar — o push é só uma camada de velocidade por cima de uma notificação que já foi salva.
Enviando uma notificação
Builder fluente (one-off)
Para notificações pontuais, sem precisar criar uma classe:
use App\Notifications\MadNotify;
MadNotify::to($userId)
->subject('Pedido aprovado')
->message('Seu pedido #123 foi aprovado e já está em produção.')
->icon('shopping-cart')
->level('success') // info | success | warning | danger
->action('PedidoForm', 'onEdit', ['id' => 123], 'Ver pedido')
->send();
Métodos do builder: subject() (ou title()), message(), icon(), level(), type(),
from(?int $userId), with(array $extra) (dados livres preservados em data), e a ação —
escolha uma:
| Método | Comportamento no clique |
|---|---|
->action($class, $method, $params, $label) |
Abre uma tela MAD via Mad.go(class, method, params) |
->navigate($url, $label) |
Navega por URL amigável via Mad.navigate(url) |
->url($url, $label) |
Abre link externo em nova aba |
to() aceita 1 destinatário, um array ou uma instância de User — send() grava 1 linha
por destinatário e retorna os models criados.
Múltiplos destinatários
MadNotify::to([$gerente->id, $supervisor->id])
->subject('Estoque baixo')
->message("O produto {$produto->nome} está com {$produto->qtd} unidades.")
->level('warning')
->send();
Notificações tipadas (reutilizáveis)
Quando o mesmo tipo de notificação é disparado em vários pontos do código, estenda
MadNotificationType em vez de repetir o builder:
<?php
namespace App\Notifications;
use App\Models\Iam\User;
class PedidoAguardandoAprovacao extends MadNotificationType
{
public function __construct(private Pedido $pedido) {}
public function toMad(?User $user): array
{
return [
'title' => _t('Aprovação de pedido'),
'message' => _t('O pedido #^1 aguarda sua aprovação.', $this->pedido->id),
'icon' => 'shopping-cart',
'level' => 'warning',
'action' => $this->goAction('PedidoForm', 'onEdit', ['id' => $this->pedido->id], _t('Ver pedido')),
];
}
}
Disparando:
use App\Notifications\MadNotify;
MadNotify::send($userId, new PedidoAguardandoAprovacao($pedido));
// ou, com vários destinatários:
MadNotify::send([$id1, $id2], new PedidoAguardandoAprovacao($pedido));
Dentro de toMad(), use os helpers protegidos goAction(), navigateAction() e
urlAction() para montar a ação — mesma semântica das três opções do builder fluente.
Trait MadNotifiable — sintaxe $user->notify()
O model App\Models\Iam\User já usa a trait App\Models\Concerns\MadNotifiable, que dá a
ergonomia familiar do Laravel sem depender do Auth::user() nativo (o MAD identifica o
usuário via session('userid')):
$user->notify(new PedidoAguardandoAprovacao($pedido));
$user->notifications(); // Builder — todas, mais recentes primeiro
$user->unreadNotifications(); // Builder — só não lidas
$user->unreadNotificationsCount(); // int
$user->markAllNotificationsRead(); // int — quantas foram marcadas
Consultando notificações
App\Models\Comm\Notification é um Eloquent model normal (conexão comm, tabela
mad_comm_notification):
use App\Models\Comm\Notification;
// Não lidas de um usuário
$notificacoes = Notification::forUser($userId)->unread()->orderByDesc('id')->limit(20)->get();
foreach ($notificacoes as $n) {
echo $n->title() . ' — ' . $n->body();
}
// Marcar como lida / não lida
$n->markRead();
$n->markUnread();
// Contar não lidas
$count = Notification::forUser($userId)->unread()->count();
Estrutura da tabela mad_comm_notification
| Coluna | Tipo | Descrição |
|---|---|---|
id |
BIGINT PK | ID |
user_to_id |
BIGINT | Destinatário |
user_from_id |
BIGINT, nullable | Remetente (default: session('userid') no momento do envio) |
type |
VARCHAR | FQCN da notificação tipada, ou 'generic' para o builder |
data |
JSON | title, message, icon, level + extras do dev (->with()) |
action |
JSON, nullable | {kind: go|navigate|url, ...} |
read_at |
TIMESTAMP, nullable | null = não lida |
created_at |
TIMESTAMP | Criação |
title(), body(), icon(), level() são acessores de conveniência que leem do JSON
data — não acesse $n->data['title'] direto em código novo.
Tempo real — Laravel Reverb
O sino do header e a central de notificações funcionam com ou sem Reverb — é uma camada de velocidade opcional, não um requisito:
| Estado | Comportamento |
|---|---|
MAD_NOTIFICATIONS_REVERB=true + Reverb no ar |
Push instantâneo: canal privado notifications.{userId}, badge + toast no cliente, sem F5 |
| Reverb desligado (default) ou indisponível | O polling nativo do header (MadTemplate.updateNotificationsMenu) atualiza o sino periodicamente |
Ative no .env:
MAD_NOTIFICATIONS_REVERB=true
Isso registra a rota de autorização do canal privado (POST /broadcasting/auth-notifications
→ NotificationBroadcastAuthController, autenticado por session('userid')) e injeta a
config do cliente Echo no layout. O broadcast em si (MadNotificationReceived) é disparado
dentro de um try/catch em MadNotify::deliver() — se o Reverb cair, a notificação
já persistida não se perde, só o push instantâneo não acontece.
No fio, o canal é private-notifications.{userId} (o PrivateChannel('notifications.'.$id)
do MadNotificationReceived ganha o prefixo private- do Echo/Pusher), e o evento é
publicado com o nome curto MadNotificationReceived (broadcastAs()) — não com o FQCN. O
authEndpoint do Echo é apontado para /broadcasting/auth-notifications pelo
ShellViewModel, separado do /broadcasting/auth usado por chat e Correio.
O evento entrega um payload pronto pra UI (já inclui unread_count recalculado):
['id', 'title', 'message', 'icon', 'level', 'action', 'created_at', 'unread_count']
Telas prontas
| Componente | Papel |
|---|---|
HeaderMessageList |
Dropdown do sino no header (lista curta + contagem) |
NotificationCenter |
Tela "ver todas" — filtro lido/não-lido, marcar (todas) como lida, excluir |
NotificationView |
Drawer de leitura de 1 notificação — abrir marca como lida e mostra o botão de ação, se houver |
Rotas auxiliares usadas pelo sino sem precisar abrir tela:
POST /app/notifications/mark-all-read
POST /app/notifications/{id}/read
Veja também
- Filas (Queue) — para notificar em lote sem travar a request.
- E-mail (MailService) — quando a notificação também precisa sair por e-mail, dispare os dois caminhos a partir do mesmo evento de domínio.