UI Enhancements in IT Service Request, Announcements, Workforce and notification fixes

This commit is contained in:
2026-03-22 18:00:10 +08:00
parent 049ab2c794
commit 872c2aab87
15 changed files with 1290 additions and 183 deletions
@@ -63,15 +63,46 @@ async function processBatch() {
return
}
// Deduplicate announcement_banner rows: for each (announcement_id, user_id)
// pair, keep only the row with the highest epoch and immediately mark older
// ones as processed without sending FCM. This prevents double-pushes caused
// by stale rows from previous epochs appearing alongside the current epoch's
// row (e.g. if scheduled_for was set to now() instead of the next boundary,
// or if a pg_cron cycle was missed leaving old rows unprocessed).
const annBannerBest = new Map<string, any>()
const staleIds: string[] = []
for (const r of rows) {
if (r.notify_type !== 'announcement_banner' || !r.announcement_id) continue
const key = `${r.announcement_id}:${r.user_id}`
const best = annBannerBest.get(key)
if (!best || (r.epoch ?? 0) > (best.epoch ?? 0)) {
if (best) staleIds.push(best.id)
annBannerBest.set(key, r)
} else {
staleIds.push(r.id)
}
}
if (staleIds.length > 0) {
console.log(`Skipping ${staleIds.length} stale announcement_banner row(s)`)
await supabase
.from('scheduled_notifications')
.update({ processed: true, processed_at: new Date().toISOString() })
.in('id', staleIds)
}
const staleSet = new Set(staleIds)
for (const r of rows.filter((r: any) => !staleSet.has(r.id))) {
try {
const scheduleId = r.schedule_id
const userId = r.user_id
const notifyType = r.notify_type
const rowId = r.id
// Build a unique ID that accounts for all reference columns + epoch
const idSource = `${scheduleId || ''}-${r.task_id || ''}-${r.it_service_request_id || ''}-${r.pass_slip_id || ''}-${userId}-${notifyType}-${r.epoch || 0}`
// Build a unique ID that accounts for all reference columns + epoch.
// announcement_id is included so that concurrent banner announcements
// targeting the same user+epoch get distinct notificationIds — without
// it, try_mark_notification_pushed would silently drop the second one.
const idSource = `${scheduleId || ''}-${r.task_id || ''}-${r.it_service_request_id || ''}-${r.pass_slip_id || ''}-${r.announcement_id || ''}-${userId}-${notifyType}-${r.epoch || 0}`
const notificationId = await uuidFromName(idSource)
// Idempotency is handled by send_fcm via try_mark_notification_pushed.
@@ -91,6 +122,7 @@ async function processBatch() {
if (r.task_id) data.task_id = r.task_id
if (r.it_service_request_id) data.it_service_request_id = r.it_service_request_id
if (r.pass_slip_id) data.pass_slip_id = r.pass_slip_id
if (r.announcement_id) data.announcement_id = r.announcement_id
switch (notifyType) {
case 'start_15':
@@ -148,6 +180,23 @@ async function processBatch() {
body = 'Your pass slip has exceeded the 1-hour limit. Please return and complete it immediately.'
data.navigate_to = '/attendance'
break
case 'announcement_banner': {
const { data: ann } = await supabase
.from('announcements')
.select('title')
.eq('id', r.announcement_id)
.single()
const rawTitle = ann?.title ?? ''
const displayTitle = rawTitle.length > 80
? rawTitle.substring(0, 80) + '\u2026'
: rawTitle
title = 'Announcement Reminder'
body = displayTitle
? `"${displayTitle}" — Please tap to review this announcement.`
: 'You have a pending announcement that requires your attention. Tap to view it.'
data.navigate_to = '/announcements'
break
}
default:
title = 'Reminder'
body = 'You have a pending notification.'