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:
@@ -51,13 +51,25 @@ class _NotificationBridgeState extends ConsumerState<NotificationBridge>
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigate to the task or ticket detail screen using GoRouter.
|
||||
void _goToDetail({String? taskId, String? ticketId}) {
|
||||
/// Navigate to the appropriate screen using GoRouter.
|
||||
///
|
||||
/// Supports generic [route] for new notification types, plus legacy
|
||||
/// [taskId], [ticketId], and [itServiceRequestId] fallbacks.
|
||||
void _goToDetail({
|
||||
String? taskId,
|
||||
String? ticketId,
|
||||
String? itServiceRequestId,
|
||||
String? route,
|
||||
}) {
|
||||
final router = ref.read(appRouterProvider);
|
||||
if (taskId != null && taskId.isNotEmpty) {
|
||||
if (route != null && route.isNotEmpty) {
|
||||
router.go(route);
|
||||
} else if (taskId != null && taskId.isNotEmpty) {
|
||||
router.go('/tasks/$taskId');
|
||||
} else if (ticketId != null && ticketId.isNotEmpty) {
|
||||
router.go('/tickets/$ticketId');
|
||||
} else if (itServiceRequestId != null && itServiceRequestId.isNotEmpty) {
|
||||
router.go('/it-service-requests/$itServiceRequestId');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,20 +108,34 @@ class _NotificationBridgeState extends ConsumerState<NotificationBridge>
|
||||
});
|
||||
}
|
||||
|
||||
/// Extract task/ticket IDs from the FCM data payload and navigate.
|
||||
/// Extract navigation target from FCM data payload and navigate.
|
||||
///
|
||||
/// The FCM data map contains keys such as `task_id` / `taskId` /
|
||||
/// `ticket_id` / `ticketId` — these are the same keys used in the
|
||||
/// background handler ([_firebaseMessagingBackgroundHandler]).
|
||||
/// Prefers the `navigate_to` field (used by scheduled notification
|
||||
/// reminders) and falls back to legacy `task_id` / `ticket_id` keys.
|
||||
void _handleMessageTap(RemoteMessage message) {
|
||||
final data = message.data;
|
||||
|
||||
final String? taskId = (data['task_id'] ?? data['taskId'] ?? data['task'])
|
||||
?.toString();
|
||||
// New: use navigate_to route if present (scheduled reminder notifications)
|
||||
final String? navigateTo = data['navigate_to']?.toString();
|
||||
if (navigateTo != null && navigateTo.isNotEmpty) {
|
||||
_goToDetail(route: navigateTo);
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy fallback for task_id / ticket_id
|
||||
final String? taskId =
|
||||
(data['task_id'] ?? data['taskId'] ?? data['task'])?.toString();
|
||||
final String? ticketId =
|
||||
(data['ticket_id'] ?? data['ticketId'] ?? data['ticket'])?.toString();
|
||||
final String? isrId =
|
||||
(data['it_service_request_id'] ?? data['itServiceRequestId'])
|
||||
?.toString();
|
||||
|
||||
_goToDetail(taskId: taskId, ticketId: ticketId);
|
||||
_goToDetail(
|
||||
taskId: taskId,
|
||||
ticketId: ticketId,
|
||||
itServiceRequestId: isrId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -138,8 +164,11 @@ class _NotificationBridgeState extends ConsumerState<NotificationBridge>
|
||||
) {
|
||||
if (next != null) {
|
||||
_goToDetail(
|
||||
route: next.route,
|
||||
taskId: next.type == 'task' ? next.id : null,
|
||||
ticketId: next.type == 'ticket' ? next.id : null,
|
||||
itServiceRequestId:
|
||||
next.type == 'it_service_request' ? next.id : null,
|
||||
);
|
||||
// Clear the pending navigation after handling
|
||||
ref.read(pendingNotificationNavigationProvider.notifier).state = null;
|
||||
|
||||
Reference in New Issue
Block a user