Schema (migration: 20260516104351_notification_isread_link):
- Notification.isRead Boolean @default(false) — tracks unread state
- Notification.link String? — deep-link URL for each notification
lib/notifier.ts:
- buildInAppBody(): per-recipient message text, context-aware
e.g. managers see "Maria Santos submitted PO-2024-12345 for your review"
submitters see "Your PO-2024-12345 has been approved"
accounts see "PO-2024-12345 approved — ready for payment"
- buildInAppLink(): routes to the correct page per recipient role
(submitter → /po/[id] or /po/[id]/receipt; manager → /approvals/[id];
accounts → /payments; etc.)
- Notifications written with both body and link on every event
API:
- GET /api/notifications — returns { unreadCount, notifications[] } for
the session user; last 20 ordered by sentAt desc
- PATCH /api/notifications/read — marks all (or specific ids) as read
NotificationBell (components/layout/notification-bell.tsx):
- Bell icon in header with red unread count badge
- Polls /api/notifications every 30 seconds
- When unread count increases vs previous tick: bounces the bell icon
and pulses the badge for 3 seconds to signal a new arrival
- Click opens dropdown panel:
- Unread items highlighted with blue-dot indicator and bolder text
- Each item is a Link to the notification's deep-link URL
- "Mark all read" button in panel header
- Auto-marks all as read when panel opens (optimistic update + API call)
- Closes on outside click
- "Showing 20 most recent" footer when list is at limit
Header: receives initialUnreadCount and initialNotifications as props
Portal layout: fetches initial notification data server-side to avoid
a loading flash on first render; dates serialised to ISO strings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>