Files
pn-new-crm/src/components/shared/realtime-toasts.tsx
Matt 4b9743a594 audit: 33-agent comprehensive audit + critical fixes
Full team audit run, all reports verbatim in docs/AUDIT-2026-05-12.md
(5900+ lines, 30+ critical findings). Already-fixed this commit:
- permission-overrides PUT: self-target block + RolePermissions allow-list + cross-tenant guard
- /api/auth/resolve-identifier: rate-limit + synthetic miss-email kill enumeration
- admin email-change: rotates account.accountId + revokes sessions
- middleware: token-gated email confirm/cancel routes whitelisted
- NAV_CATALOG: 10 dead-link sweeps to existing /admin/<x> targets

Feature work landing same commit: optional username sign-in
(migration 0054), per-user permission overrides (0055) with three-state
matrix tabbed inside UserForm, user disable button, role + outcome +
stage label normalisation across the platform, admin email-change
with auto-notification template.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 16:52:35 +02:00

85 lines
2.8 KiB
TypeScript

'use client';
import { useEffect } from 'react';
import { toast } from 'sonner';
import { useSocket } from '@/providers/socket-provider';
import { stageLabel, formatOutcome } from '@/lib/constants';
/**
* App-wide subscriber that surfaces high-signal sales events as sonner
* toasts. Mounted once inside SocketProvider so reps see "EOI signed",
* "Deposit recorded", "Stage advanced" without having to refresh.
*
* Render-only - no children. Intentionally narrow in scope: only toast on
* events that are noteworthy *to a user staring at any page*. Per-page
* cache invalidations stay in `useRealtimeInvalidation`.
*/
export function RealtimeToasts() {
const socket = useSocket();
useEffect(() => {
if (!socket) return;
function onStageChanged(payload: {
newStage?: string;
oldStage?: string | null;
clientName?: string;
}) {
if (!payload?.newStage) return;
const who = payload.clientName?.trim() || 'an interest';
toast.success(`${who}${stageLabel(payload.newStage)}`, {
description: payload.oldStage
? `Advanced from ${stageLabel(payload.oldStage)}.`
: 'Pipeline stage advanced.',
});
}
function onDocumentCompleted(payload: { type?: string }) {
// Kick a generic "fully signed" - the type-specific message is
// friendlier when we can identify it as an EOI.
if (payload?.type === 'eoi') {
toast.success('EOI fully signed', {
description: 'All required signatures received.',
});
} else {
toast.success('Document fully signed');
}
}
function onSignerSigned(payload: { signerName?: string; documentTitle?: string }) {
const who = payload?.signerName?.trim();
const title = payload?.documentTitle?.trim();
if (who && title) {
toast.message(`${who} signed "${title}"`);
} else if (who) {
toast.message(`${who} signed a document`);
} else {
toast.message('Signer added a signature');
}
}
function onOutcomeSet(payload: { outcome?: string }) {
if (!payload?.outcome) return;
const isWon = payload.outcome === 'won';
const label = formatOutcome(payload.outcome) ?? payload.outcome;
const fn = isWon ? toast.success : toast.message;
fn(`Interest closed — ${label}`);
}
socket.on('interest:stageChanged', onStageChanged);
socket.on('document:completed', onDocumentCompleted);
socket.on('document:signer:signed', onSignerSigned);
socket.on('interest:outcomeSet', onOutcomeSet);
return () => {
socket.off('interest:stageChanged', onStageChanged);
socket.off('document:completed', onDocumentCompleted);
socket.off('document:signer:signed', onSignerSigned);
socket.off('interest:outcomeSet', onOutcomeSet);
};
}, [socket]);
return null;
}