feat: real notifications in the sidebar bell

Auto-generate notifications on patient record create/update (fan out to
the other clinic members, pushed live), and wire the sidebar bell to real
data via a useNotifications hook over the shared socket: live unread badge,
real list, mark-all-read on open. Drops the hardcoded sample array and the
dead "View all" link.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-07 21:29:57 +03:00
parent 730e07fcfc
commit 288d14758f
6 changed files with 189 additions and 60 deletions
@@ -56,7 +56,7 @@
"activity": "Activity",
"settings": "Settings",
"notifications": "Notifications",
"viewAllNotifications": "View all notifications",
"notificationsEmpty": "You're all caught up.",
"quickNav": "Quick nav",
"commandGroup": "Go to",
"commandPlaceholder": "Search pages…",
+32
View File
@@ -0,0 +1,32 @@
import { apiFetch } from "@/lib/api-client";
// A notification for the signed-in user. Mirrors the backend
// `src/types/notification.ts`.
export type Notification = {
id: string;
type: string;
text: string;
read: boolean;
entityType: string | null;
entityId: string | null;
actorName: string | null;
actorInitials: string | null;
createdAt: string;
};
export type NotificationsResponse = {
notifications: Notification[];
unread: number;
};
export function listNotifications(): Promise<NotificationsResponse> {
return apiFetch<NotificationsResponse>("/api/notifications");
}
export function markNotificationRead(id: string): Promise<void> {
return apiFetch<void>(`/api/notifications/${id}/read`, { method: "PATCH" });
}
export function markAllNotificationsRead(): Promise<void> {
return apiFetch<void>("/api/notifications/read-all", { method: "POST" });
}
+52
View File
@@ -0,0 +1,52 @@
import { useEffect, useState } from "react";
import {
type Notification,
listNotifications,
markAllNotificationsRead,
} from "@/lib/notifications";
import { getSocket } from "@/lib/socket";
// Loads the signed-in user's notifications and keeps them live via the shared
// socket ("notification:new"). Exposes a markAllRead action for the popover.
export function useNotifications() {
const [items, setItems] = useState<Notification[]>([]);
const [unread, setUnread] = useState(0);
useEffect(() => {
let active = true;
listNotifications()
.then((res) => {
if (active) {
setItems(res.notifications);
setUnread(res.unread);
}
})
.catch(() => {
/* api-client redirects on 401 */
});
const socket = getSocket();
const onNew = (n: Notification) => {
setItems((prev) => [n, ...prev].slice(0, 30));
setUnread((u) => u + 1);
};
socket.on("notification:new", onNew);
return () => {
active = false;
socket.off("notification:new", onNew);
};
}, []);
const markAllRead = async () => {
setItems((prev) => prev.map((n) => ({ ...n, read: true })));
setUnread(0);
try {
await markAllNotificationsRead();
} catch {
/* best-effort */
}
};
return { items, unread, markAllRead };
}