From 5d16602cebdecb96867409cf0f334c1a1645c1bd Mon Sep 17 00:00:00 2001 From: 230417 <230417@epvc.pt> Date: Wed, 25 Mar 2026 12:08:09 +0000 Subject: [PATCH] feat: Implement a new push notification service and enhance appointment reminder functionality with new database columns and logic. --- .../functions/send-push-notification/index.ts | 71 +++++++++++++++++++ supabase/functions/send-reminder/index.ts | 60 +++++++++------- supabase/migrations/update_reminders.sql | 13 ++++ 3 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 supabase/functions/send-push-notification/index.ts create mode 100644 supabase/migrations/update_reminders.sql diff --git a/supabase/functions/send-push-notification/index.ts b/supabase/functions/send-push-notification/index.ts new file mode 100644 index 0000000..1e7bf81 --- /dev/null +++ b/supabase/functions/send-push-notification/index.ts @@ -0,0 +1,71 @@ +import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' + +const EXPO_PUSH_URL = 'https://exp.host/--/api/v2/push/send'; + +Deno.serve(async (req) => { + const supabase = createClient( + Deno.env.get('SUPABASE_URL') ?? '', + Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' + ) + + try { + const payload = await req.json(); + // Payload do Webhook do Supabase: { record: { user_id, message, ... } } + const { record } = payload; + + if (!record || !record.user_id) { + return new Response("No record found", { status: 400 }); + } + + // 1. Obter o token do utilizador + const { data: profile, error } = await supabase + .from('profiles') + .select('fcm_token') + .eq('id', record.user_id) + .single(); + + if (error || !profile?.fcm_token) { + console.log(`No token found for user ${record.user_id}`); + return new Response("No token found", { status: 200 }); + } + + const expoToken = profile.fcm_token; + if (!expoToken.startsWith('ExponentPushToken')) { + console.log(`Invalid Expo token for user ${record.user_id}`); + return new Response("Invalid token", { status: 200 }); + } + + // 2. Enviar para o Expo + const notification = { + to: expoToken, + sound: 'default', + title: 'Smart Agenda', + body: record.message, + data: { notificationId: record.id }, + }; + + const response = await fetch(EXPO_PUSH_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + body: JSON.stringify(notification), + }); + + const result = await response.json(); + console.log('Expo Result:', result); + + return new Response(JSON.stringify({ success: true }), { + headers: { "Content-Type": "application/json" }, + status: 200, + }); + + } catch (err) { + console.error('Error sending push notification:', err); + return new Response(JSON.stringify({ error: err.message }), { + headers: { "Content-Type": "application/json" }, + status: 500, + }); + } +}) diff --git a/supabase/functions/send-reminder/index.ts b/supabase/functions/send-reminder/index.ts index 84f4416..72ce281 100644 --- a/supabase/functions/send-reminder/index.ts +++ b/supabase/functions/send-reminder/index.ts @@ -1,68 +1,80 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' -// URL da API de Push do Expo (não precisa de chaves de servidor para o básico) const EXPO_PUSH_URL = 'https://exp.host/--/api/v2/push/send'; Deno.serve(async (req) => { - // Criar o cliente Supabase com as variáveis de ambiente do sistema const supabase = createClient( Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_SERVICE_ROLE_KEY') ?? '' ) try { - // 1. Procurar agendamentos pendentes com tokens válidos - // Nota: O campo 'fcm_token' na BD irá guardar o 'ExponentPushToken' + const now = new Date(); + + // 1. Buscar agendamentos que: + // - Estão pendentes + // - Ainda não enviaram lembrete (field 'reminder_sent' deve existir) + // - O tempo para o agendamento é <= reminder_minutes const { data: appts, error } = await supabase .from('appointments') - .select('*, profiles(fcm_token)') + .select('*, profiles(fcm_token), shops(name)') .eq('status', 'pendente') + .eq('reminder_sent', false) .not('profiles.fcm_token', 'is', null); if (error) throw error; const notifications = []; + const appointmentsToUpdate = []; for (const appt of appts) { - const expoToken = appt.profiles?.fcm_token; - - // Verifica se é um token válido do Expo - if (expoToken && expoToken.startsWith('ExponentPushToken')) { - notifications.push({ - to: expoToken, - sound: 'default', - title: 'Lembrete Smart Agenda', - body: `Olá! Não se esqueça do seu agendamento em breve.`, - data: { appointmentId: appt.id }, - }); + const apptDate = new Date(appt.date.replace(' ', 'T')); // Converte "YYYY-MM-DD HH:mm" para ISO + const diffInMinutes = (apptDate.getTime() - now.getTime()) / (1000 * 60); + const reminderMinutes = appt.reminder_minutes || 1440; // Default 24h + + // Se estiver dentro da janela de lembrete (por exemplo, se faltarem 10min e o lembrete for de 10min) + // Ajustamos uma margem de segurança de 5 min para o Cron não perder a janela + if (diffInMinutes <= reminderMinutes && diffInMinutes > -10) { + const expoToken = appt.profiles?.fcm_token; + if (expoToken && expoToken.startsWith('ExponentPushToken')) { + notifications.push({ + to: expoToken, + sound: 'default', + title: `Lembrete: ${appt.shops?.name}`, + body: `O seu agendamento está a aproximar-se (${appt.date.split(' ')[1]}).`, + data: { appointmentId: appt.id }, + }); + appointmentsToUpdate.push(appt.id); + } } } - console.log(`Enviando ${notifications.length} notificações via Expo...`); + console.log(`Sending ${notifications.length} reminders...`); if (notifications.length > 0) { - // 2. Enviar notificações em lote (Batch) via Expo - const response = await fetch(EXPO_PUSH_URL, { + await fetch(EXPO_PUSH_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', - 'Accept-Encoding': 'gzip, deflate', }, body: JSON.stringify(notifications), }); - const result = await response.json(); - console.log('Resultado do Expo:', result); + // Marcar como enviado para não repetir + await supabase + .from('appointments') + .update({ reminder_sent: true }) + .in('id', appointmentsToUpdate); } - return new Response(JSON.stringify({ success: true, sent: notifications.length }), { + return new Response(JSON.stringify({ success: true, count: notifications.length }), { headers: { "Content-Type": "application/json" }, status: 200, }); } catch (err) { - console.error('Erro na função send-reminder:', err); + console.error('Error in send-reminder:', err); return new Response(JSON.stringify({ error: err.message }), { headers: { "Content-Type": "application/json" }, status: 500, diff --git a/supabase/migrations/update_reminders.sql b/supabase/migrations/update_reminders.sql new file mode 100644 index 0000000..a3bf9eb --- /dev/null +++ b/supabase/migrations/update_reminders.sql @@ -0,0 +1,13 @@ +-- 1. Adicionar coluna para controlo de lembretes enviados +-- Isto evita que o utilizador receba o mesmo lembrete várias vezes +ALTER TABLE appointments +ADD COLUMN IF NOT EXISTS reminder_sent BOOLEAN DEFAULT false; + +-- 2. Garantir que a coluna reminder_minutes existe (caso não tenha sido criada no outro PC) +ALTER TABLE appointments +ADD COLUMN IF NOT EXISTS reminder_minutes INTEGER DEFAULT 1440; + +-- 3. Criar índice para performance na busca de lembretes pendentes +CREATE INDEX IF NOT EXISTS idx_appointments_reminder_status +ON appointments (status, reminder_sent) +WHERE status = 'pendente' AND reminder_sent = false;