Files
pn-new-crm/src/components/shared/realtime-toasts.tsx
Matt 221ae5784e chore(autonomous-session): consolidate uncommitted work from prior session
Bundles the prior autonomous-session output that was sitting unstaged:

- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
  never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
  after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
  redirects (ocr to ai, reports to dashboard, invitations to users),
  docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
  flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
  let-reassign), set-state-in-effect disables in CountryFlag and
  UploadForSigning preview-bytes effect, unused 'confirm' destructures in
  interest contract + reservation tabs, unescaped apostrophe in test-template
  card copy
2026-05-23 00:52:59 +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;
}