UI Enhancements in IT Service Request, Announcements, Workforce and notification fixes
This commit is contained in:
@@ -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.'
|
||||
|
||||
@@ -38,11 +38,17 @@ Deno.serve(async (req) => {
|
||||
})
|
||||
}
|
||||
|
||||
// Optional Idempotency (if you pass notification_id inside the data payload)
|
||||
// Idempotency: if notification_id is provided, use try_mark_notification_pushed
|
||||
// to ensure at-most-once delivery even under concurrent edge-function invocations.
|
||||
if (payload.data && payload.data.notification_id) {
|
||||
const { data: markData, error: markErr } = await supabase
|
||||
.rpc('try_mark_notification_pushed', { p_notification_id: payload.data.notification_id })
|
||||
|
||||
|
||||
if (markErr) {
|
||||
console.error('try_mark_notification_pushed RPC error, skipping to be safe:', markErr)
|
||||
return new Response('Idempotency check failed', { status: 200, headers: corsHeaders })
|
||||
}
|
||||
|
||||
if (markData === false) {
|
||||
console.log('Notification already pushed, skipping:', payload.data.notification_id)
|
||||
return new Response('Already pushed', { status: 200, headers: corsHeaders })
|
||||
|
||||
Reference in New Issue
Block a user