mirror of
https://github.com/temetro/temetro.git
synced 2026-09-04 14:55:28 +00:00
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:
@@ -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…",
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user