Improve notification deep-linking and add Arabic meeting reminders

Enhance notification handling to preserve deep-link parameters, add relatedId and relatedType to notifications for better association, and implement Arabic pluralization for meeting reminders.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 9abb57f5-4f2c-4eed-9fad-ab278885083e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/fKZh36i
Replit-Helium-Checkpoint-Created: true
This commit is contained in:
Replit Agent
2026-06-01 14:48:26 +00:00
parent 7651a23372
commit ceadff6944
9 changed files with 264 additions and 13 deletions
@@ -222,7 +222,7 @@ export async function broadcastExecutiveMeetingNotifications(
type: "executive_meeting",
relatedId: meetingId,
tag: meetingId ? `meeting-${meetingId}-${notificationType}` : `meeting-${notificationType}`,
url: "/meetings",
url: meetingId ? `/meetings?meeting=${meetingId}` : "/meetings",
});
}
io.emit("executive_meeting_notifications_changed", {
@@ -43,6 +43,16 @@ function stripHtmlToPlainText(input: string | null | undefined): string {
return decoded.replace(/\s+/g, " ").trim();
}
// Arabic minute pluralisation for the "starts after N minutes" reminder
// body. Arabic uses three forms for small counts: 1 -> دقيقة, 2 -> دقيقتين
// (dual), 3+ -> دقائق. The scheduler only fires for deltas of 1-5 min, but
// the helper stays correct for the dual/plural boundary regardless.
function minutesAfterAr(n: number): string {
if (n <= 1) return "بعد دقيقة";
if (n === 2) return "بعد دقيقتين";
return `بعد ${n} دقائق`;
}
function dateKey(d: Date): string {
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
}
@@ -161,7 +171,7 @@ async function tickOnce(): Promise<void> {
stripHtmlToPlainText(meeting.titleEn) ||
stripHtmlToPlainText(meeting.titleAr) ||
"Meeting";
const bodyAr = `${subjectAr} يبدأ خلال ${deltaMin} دقيقة`;
const bodyAr = `${subjectAr} يبدأ ${minutesAfterAr(deltaMin)}`;
const bodyEn = `${subjectEn} starts in ${deltaMin} min`;
for (const { userId } of claimed) {
@@ -175,7 +185,7 @@ async function tickOnce(): Promise<void> {
bodyAr,
bodyEn,
tag: `em-reminder-${meeting.id}`,
url: "/meetings",
url: `/meetings?meeting=${meeting.id}`,
type: "executive_meeting",
relatedId: meeting.id,
},
+6 -2
View File
@@ -1130,6 +1130,8 @@ router.post("/notes/:id/send", requireAuth, async (req, res): Promise<void> => {
bodyAr: `${senderNameAr} أرسل لك ملاحظة`,
bodyEn: `${senderName} sent you a note`,
type: "note",
relatedType: "note",
relatedId: note.id,
})
.returning();
await emitToUser(r.recipientUserId, "notification_created", { ...notif, type: "note" });
@@ -1143,7 +1145,7 @@ router.post("/notes/:id/send", requireAuth, async (req, res): Promise<void> => {
type: "note",
relatedId: note.id,
tag: `note-${note.id}`,
url: "/notes",
url: `/notes?thread=${note.id}`,
});
await emitToUser(r.recipientUserId, "note_received", {
noteId: note.id,
@@ -1488,6 +1490,8 @@ router.post("/notes/:id/reply", requireAuth, async (req, res): Promise<void> =>
bodyAr: `${replierNameAr} رد على ملاحظة`,
bodyEn: `${replierName} replied on a note`,
type: "note",
relatedType: "note",
relatedId: id.id,
})
.returning();
await emitToUser(otherPartyUserId, "notification_created", { ...notif, type: "note" });
@@ -1501,7 +1505,7 @@ router.post("/notes/:id/reply", requireAuth, async (req, res): Promise<void> =>
type: "note",
relatedId: id.id,
tag: `note-reply-${reply.id}`,
url: "/notes",
url: `/notes?thread=${id.id}`,
});
// Enrich the realtime payload so the recipient's client can render the
// floating reply card without an extra round-trip. We truncate the body
@@ -117,9 +117,19 @@ async function notifyUser(
bodyEn: string,
orderId: number,
actorId?: number,
// Which order surface the deep-link should open: the recipient's own
// "My orders" list (status/cancel updates) or the "Incoming orders"
// queue (a brand-new order to claim). Drives both the stored
// notification's relatedType and the Web Push deep-link URL so a tap
// lands on the right page with the order highlighted.
target: "mine" | "incoming" = "mine",
) {
// Never notify users about actions they themselves initiated.
if (actorId !== undefined && actorId === userId) return;
const relatedType = target === "incoming" ? "incoming_order" : "order";
const deepLinkUrl = `${
target === "incoming" ? "/orders/incoming" : "/my-orders"
}?order=${orderId}`;
const [notification] = await db
.insert(notificationsTable)
.values({
@@ -130,7 +140,7 @@ async function notifyUser(
bodyEn,
type: "order",
relatedId: orderId,
relatedType: "order",
relatedType,
})
.returning();
await emitToUser(userId, "notification_created", notification);
@@ -154,7 +164,7 @@ async function notifyUser(
type: "order",
relatedId: orderId,
tag: `order-${orderId}`,
url: "/my-orders",
url: deepLinkUrl,
},
{ ignoreConnected: true },
);
@@ -209,6 +219,7 @@ router.post("/orders", requireAuth, async (req, res): Promise<void> => {
service.nameEn,
order.id,
userId,
"incoming",
);
}
await broadcastIncomingChanged(receivers);