Implement push notification reminder system with 9 notification types
Adds comprehensive push notification reminders using pg_cron + pg_net: - Shift check-in reminder (15 min before, with countdown banner) - Shift check-out reminder (hourly, persistent until checkout) - Overtime idle reminder (15 min without task) - Overtime checkout reminder (30 min after task completion) - IT service request event reminder (1 hour before event) - IT service request evidence reminder (daily) - Paused task reminder (daily) - Backlog reminder (15 min before shift end) - Pass slip expiry reminder (15 min before 1-hour limit, with countdown banner) Database: Extended scheduled_notifications table to support polymorphic references (schedule_id, task_id, it_service_request_id, pass_slip_id) with unique constraint and epoch column for deduplication. Implemented 8 enqueue functions + master dispatcher. Uses pg_cron every minute to enqueue and pg_net to trigger process_scheduled_notifications edge function, eliminating need for external cron job. Credentials stored in vault with GUC fallback for flexibility. Flutter: Added ShiftCountdownBanner and PassSlipCountdownBanner widgets that display persistent countdown timers for active shifts and pass slips. Both auto-dismiss when user completes the action. FCM handler triggers shift countdown on start_15 messages. navigate_to field in data payload enables flexible routing to any screen. Edge function: Updated process_scheduled_notifications to handle all 9 types with appropriate titles, bodies, and routing. Includes pass_slip_id in idempotency tracking. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -54,7 +54,9 @@ async function processBatch() {
|
||||
const notifyType = r.notify_type
|
||||
const rowId = r.id
|
||||
|
||||
const notificationId = await uuidFromName(`${scheduleId}-${userId}-${notifyType}`)
|
||||
// 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}`
|
||||
const notificationId = await uuidFromName(idSource)
|
||||
|
||||
// Attempt to mark idempotent push
|
||||
const { data: markData, error: markErr } = await supabase.rpc('try_mark_notification_pushed', { p_notification_id: notificationId })
|
||||
@@ -71,18 +73,74 @@ async function processBatch() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Prepare message
|
||||
// Prepare message based on notify_type
|
||||
let title = ''
|
||||
let body = ''
|
||||
if (notifyType === 'start_15') {
|
||||
title = 'Shift starting soon'
|
||||
body = 'Your shift starts in 15 minutes. Don\'t forget to check in.'
|
||||
} else if (notifyType === 'end') {
|
||||
title = 'Shift ended'
|
||||
body = 'Your shift has ended. Please remember to check out if you haven\'t.'
|
||||
} else {
|
||||
title = 'Shift reminder'
|
||||
body = 'Reminder about your shift.'
|
||||
const data: Record<string, string> = {
|
||||
notification_id: notificationId,
|
||||
type: notifyType,
|
||||
}
|
||||
|
||||
// Include reference IDs in data payload
|
||||
if (scheduleId) data.schedule_id = scheduleId
|
||||
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
|
||||
|
||||
switch (notifyType) {
|
||||
case 'start_15':
|
||||
title = 'Shift starting soon'
|
||||
body = "Your shift starts in 15 minutes. Don't forget to check in."
|
||||
data.navigate_to = '/attendance'
|
||||
break
|
||||
case 'end':
|
||||
title = 'Shift ended'
|
||||
body = "Your shift has ended. Please remember to check out."
|
||||
data.navigate_to = '/attendance'
|
||||
break
|
||||
case 'end_hourly':
|
||||
title = 'Check-out reminder'
|
||||
body = "You haven't checked out yet. Please check out when done."
|
||||
data.navigate_to = '/attendance'
|
||||
break
|
||||
case 'overtime_idle_15':
|
||||
title = 'No active task'
|
||||
body = "You've been on overtime for 15 minutes without an active task or IT service request."
|
||||
data.navigate_to = '/tasks'
|
||||
break
|
||||
case 'overtime_checkout_30':
|
||||
title = 'Overtime check-out reminder'
|
||||
body = "It's been 30 minutes since your last task ended. Consider checking out if you're done."
|
||||
data.navigate_to = '/attendance'
|
||||
break
|
||||
case 'isr_event_60':
|
||||
title = 'IT Service Request event soon'
|
||||
body = 'An IT service request event starts in 1 hour.'
|
||||
data.navigate_to = `/it-service-requests/${r.it_service_request_id}`
|
||||
break
|
||||
case 'isr_evidence_daily':
|
||||
title = 'Evidence upload reminder'
|
||||
body = 'Please upload evidence and action taken for your IT service request.'
|
||||
data.navigate_to = `/it-service-requests/${r.it_service_request_id}`
|
||||
break
|
||||
case 'task_paused_daily':
|
||||
title = 'Paused task reminder'
|
||||
body = 'You have a paused task that needs attention.'
|
||||
data.navigate_to = `/tasks/${r.task_id}`
|
||||
break
|
||||
case 'backlog_15':
|
||||
title = 'Pending tasks reminder'
|
||||
body = 'Your shift ends in 15 minutes and you still have pending tasks.'
|
||||
data.navigate_to = '/tasks'
|
||||
break
|
||||
case 'pass_slip_expiry_15':
|
||||
title = 'Pass slip expiring soon'
|
||||
body = 'Your pass slip expires in 15 minutes. Please return and complete it.'
|
||||
data.navigate_to = '/attendance'
|
||||
break
|
||||
default:
|
||||
title = 'Reminder'
|
||||
body = 'You have a pending notification.'
|
||||
}
|
||||
|
||||
// Call send_fcm endpoint to deliver push (reuses existing implementation)
|
||||
@@ -90,11 +148,7 @@ async function processBatch() {
|
||||
user_ids: [userId],
|
||||
title,
|
||||
body,
|
||||
data: {
|
||||
notification_id: notificationId,
|
||||
schedule_id: scheduleId,
|
||||
type: notifyType,
|
||||
},
|
||||
data,
|
||||
}
|
||||
|
||||
const res = await fetch(SEND_FCM_URL, {
|
||||
|
||||
Reference in New Issue
Block a user