mirror of
https://github.com/temetro/temetro.git
synced 2026-08-24 08:46:27 +00:00
feat: real-time staff messaging over Socket.io
Add conversations/participants/messages tables, a participant-scoped REST API (/api/conversations) and a Socket.io server (session-authenticated handshake; per-user + per-conversation rooms) sharing the HTTP port. New messages broadcast live and create per-recipient notifications. Also lands the notifications table + service + routes (used by the message flow). The Messages page is rewritten: live threads, unread state, and a compose dialog to start a conversation with a clinic member. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
|
||||
import { db } from "../db/index.js";
|
||||
import { notifications } from "../db/schema/notifications.js";
|
||||
import type { Notification } from "../types/notification.js";
|
||||
import { initialsOf } from "./activity.js";
|
||||
|
||||
type NotificationRow = typeof notifications.$inferSelect;
|
||||
|
||||
const UUID_RE =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
|
||||
function toNotification(row: NotificationRow): Notification {
|
||||
return {
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
text: row.text,
|
||||
read: row.read,
|
||||
entityType: row.entityType,
|
||||
entityId: row.entityId,
|
||||
actorName: row.actorName,
|
||||
actorInitials: row.actorInitials,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Best-effort: a notification must never fail the originating action. Returns the
|
||||
// created notification (so the caller can push it over the socket) or null.
|
||||
export async function createNotification(params: {
|
||||
orgId: string;
|
||||
userId: string;
|
||||
type: string;
|
||||
text: string;
|
||||
entityType?: string | null;
|
||||
entityId?: string | null;
|
||||
actorName?: string | null;
|
||||
}): Promise<Notification | null> {
|
||||
try {
|
||||
const [row] = await db
|
||||
.insert(notifications)
|
||||
.values({
|
||||
organizationId: params.orgId,
|
||||
userId: params.userId,
|
||||
type: params.type,
|
||||
text: params.text,
|
||||
entityType: params.entityType ?? null,
|
||||
entityId: params.entityId ?? null,
|
||||
actorName: params.actorName ?? null,
|
||||
actorInitials: params.actorName ? initialsOf(params.actorName) : null,
|
||||
})
|
||||
.returning();
|
||||
return row ? toNotification(row) : null;
|
||||
} catch (err) {
|
||||
console.error("Failed to create notification:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function listNotifications(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
limit = 30,
|
||||
): Promise<{ notifications: Notification[]; unread: number }> {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(notifications)
|
||||
.where(
|
||||
and(
|
||||
eq(notifications.organizationId, orgId),
|
||||
eq(notifications.userId, userId),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(notifications.createdAt))
|
||||
.limit(limit);
|
||||
const unread = rows.filter((r) => !r.read).length;
|
||||
return { notifications: rows.map(toNotification), unread };
|
||||
}
|
||||
|
||||
export async function markRead(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
id: string,
|
||||
): Promise<boolean> {
|
||||
if (!UUID_RE.test(id)) return false;
|
||||
const updated = await db
|
||||
.update(notifications)
|
||||
.set({ read: true })
|
||||
.where(
|
||||
and(
|
||||
eq(notifications.id, id),
|
||||
eq(notifications.organizationId, orgId),
|
||||
eq(notifications.userId, userId),
|
||||
),
|
||||
)
|
||||
.returning({ id: notifications.id });
|
||||
return updated.length > 0;
|
||||
}
|
||||
|
||||
export async function markAllRead(
|
||||
orgId: string,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
await db
|
||||
.update(notifications)
|
||||
.set({ read: true })
|
||||
.where(
|
||||
and(
|
||||
eq(notifications.organizationId, orgId),
|
||||
eq(notifications.userId, userId),
|
||||
eq(notifications.read, false),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user