feat: Implement a new push notification service and enhance appointment reminder functionality with new database columns and logic.

This commit is contained in:
2026-03-25 12:08:09 +00:00
parent cb1cf4335b
commit 5d16602ceb
3 changed files with 120 additions and 24 deletions

View File

@@ -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,
});
}
})

View File

@@ -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,

View File

@@ -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;