From 0fcc7ae97b46330aae6073f2d3aa5bc57fdfce16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Deir=C3=B3=20Rodrigues?= Date: Mon, 10 Nov 2025 15:36:00 -0300 Subject: [PATCH 1/2] =?UTF-8?q?configura=C3=A7=C3=A3o=20do=20envio=20de=20?= =?UTF-8?q?sms?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/schedule/schedule-form.tsx | 121 +++++++++++++++++++------- services/appointmentsApi.mjs | 3 + 2 files changed, 91 insertions(+), 33 deletions(-) diff --git a/components/schedule/schedule-form.tsx b/components/schedule/schedule-form.tsx index becac35..af89a24 100644 --- a/components/schedule/schedule-form.tsx +++ b/components/schedule/schedule-form.tsx @@ -207,46 +207,101 @@ export default function ScheduleForm() { }, [selectedDoctor, selectedDate, fetchAvailableSlots]); // 🔹 Submeter agendamento - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); + // 🔹 Submeter agendamento + // 🔹 Submeter agendamento +// 🔹 Submeter agendamento +// 🔹 Submeter agendamento +const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); - const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role); - const patientId = isSecretaryLike ? selectedPatient : userId; + const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role); + const patientId = isSecretaryLike ? selectedPatient : userId; - if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) { - toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." }); - return; - } + if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) { + toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." }); + return; + } + + try { + const body = { + doctor_id: selectedDoctor, + patient_id: patientId, + scheduled_at: `${selectedDate}T${selectedTime}:00`, + duration_minutes: Number(duracao), + notes, + appointment_type: tipoConsulta, + }; + + // ✅ mantém o fluxo original de criação (funcional) + await appointmentsService.create(body); + + const dateFormatted = selectedDate.split("-").reverse().join("/"); + + toast({ + title: "Consulta agendada!", + description: `Consulta marcada para ${dateFormatted} às ${selectedTime} com o(a) médico(a) ${ + doctors.find((d) => d.id === selectedDoctor)?.full_name || "" + }.`, + }); + + // 📞 busca o telefone corretamente + let phoneNumber = "+5511999999999"; // fallback try { - const body = { - doctor_id: selectedDoctor, - patient_id: patientId, - scheduled_at: `${selectedDate}T${selectedTime}:00`, - duration_minutes: Number(duracao), - notes, - appointment_type: tipoConsulta, - }; + if (isSecretaryLike) { + // se for secretária/admin → usa paciente selecionado + const patient = patients.find((p: any) => p.id === patientId); + if (patient?.phone_number) phoneNumber = patient.phone_number; + } else { + // se for paciente → usa o service do próprio user + const me = await usersService.getMe(); + if (me?.profile?.phone) phoneNumber = me.profile.phone; + } - await appointmentsService.create(body); - const dateFormatted = selectedDate.split("-").reverse().join("/"); - toast({ - title: "Consulta agendada!", - description: `Consulta marcada para ${dateFormatted} às ${selectedTime} com o(a) médico(a) ${ - doctors.find((d) => d.id === selectedDoctor)?.full_name || "" - }.`, - }); - - setSelectedDoctor(""); - setSelectedDate(""); - setSelectedTime(""); - setNotes(""); - setSelectedPatient(""); + // padroniza número para formato internacional (+55) + if (phoneNumber) { + phoneNumber = phoneNumber.replace(/\D/g, ""); // remove caracteres não numéricos + if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}`; + phoneNumber = `+${phoneNumber}`; + } } catch (err) { - console.error(err); - toast({ title: "Erro", description: "Falha ao agendar consulta." }); + console.warn("Não foi possível obter telefone do paciente:", err); } - }; + + // 💬 envia o SMS de confirmação + // 💬 Envia o SMS de lembrete (sem mostrar nada ao paciente) +try { + const smsRes = await appointmentsService.send_sms({ + phone_number: phoneNumber, + message: `Lembrete: sua consulta é em ${dateFormatted} às ${selectedTime} na Clínica MediConnect.`, + patient_id: patientId, + }); + + if (smsRes?.success) { + console.log("✅ SMS enviado com sucesso:", smsRes.message_sid || smsRes.sid || "(sem SID retornado)"); + } else { + console.warn("⚠️ Falha no envio do SMS:", smsRes); + } +} catch (smsErr) { + console.error("❌ Erro ao enviar SMS:", smsErr); +} + + + // 🧹 limpa os campos + setSelectedDoctor(""); + setSelectedDate(""); + setSelectedTime(""); + setNotes(""); + setSelectedPatient(""); + } catch (err) { + console.error("❌ Erro ao agendar consulta:", err); + toast({ title: "Erro", description: "Falha ao agendar consulta." }); + } +}; + + + + // 🔹 Tooltip no calendário useEffect(() => { diff --git a/services/appointmentsApi.mjs b/services/appointmentsApi.mjs index 5180c6e..bebf986 100644 --- a/services/appointmentsApi.mjs +++ b/services/appointmentsApi.mjs @@ -45,4 +45,7 @@ export const appointmentsService = { * @returns {Promise} - Uma promessa que resolve com a resposta da API. */ delete: (id) => api.delete(`/rest/v1/appointments?id=eq.${id}`), + + send_sms: (data) => api.post("/functions/v1/send-sms", data) + }; \ No newline at end of file From 866e15df9ee9a71a756397047c62813ed02dab25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lucas=20Deir=C3=B3=20Rodrigues?= Date: Tue, 11 Nov 2025 00:53:04 -0300 Subject: [PATCH 2/2] envio de sms ao agendar consulta --- components/schedule/schedule-form.tsx | 65 +++++++++++++++++---------- services/Sms.mjs | 58 ++++++++++++++++++++++++ services/api.mjs | 15 +++++-- services/appointmentsApi.mjs | 2 +- 4 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 services/Sms.mjs diff --git a/components/schedule/schedule-form.tsx b/components/schedule/schedule-form.tsx index af89a24..89ffb05 100644 --- a/components/schedule/schedule-form.tsx +++ b/components/schedule/schedule-form.tsx @@ -14,8 +14,10 @@ import { Textarea } from "@/components/ui/textarea"; import { Calendar as CalendarShadcn } from "@/components/ui/calendar"; import { format, addDays } from "date-fns"; import { User, StickyNote, Calendar } from "lucide-react"; +import {smsService } from "@/services/Sms.mjs" import { toast } from "@/hooks/use-toast"; + export default function ScheduleForm() { // Estado do usuário e role const [role, setRole] = useState("paciente"); @@ -244,41 +246,56 @@ const handleSubmit = async (e: React.FormEvent) => { }.`, }); - // 📞 busca o telefone corretamente - let phoneNumber = "+5511999999999"; // fallback +let phoneNumber = "+5511999999999"; // fallback - try { - if (isSecretaryLike) { - // se for secretária/admin → usa paciente selecionado - const patient = patients.find((p: any) => p.id === patientId); - if (patient?.phone_number) phoneNumber = patient.phone_number; - } else { - // se for paciente → usa o service do próprio user - const me = await usersService.getMe(); - if (me?.profile?.phone) phoneNumber = me.profile.phone; - } +try { + if (isSecretaryLike) { + // Secretária/admin → telefone do paciente selecionado + const patient = patients.find((p: any) => p.id === patientId); + + // Pacientes criados no sistema podem ter phone ou phone_mobile + const rawPhone = patient?.phone || patient?.phone_mobile || null; + + if (rawPhone) phoneNumber = rawPhone; + } else { + // Paciente → telefone vem do perfil do próprio usuário logado + const me = await usersService.getMe(); + + +const rawPhone = + me?.profile?.phone || + (typeof me?.profile === "object" && "phone_mobile" in me.profile ? (me.profile as any).phone_mobile : null) || + (typeof me === "object" && "user_metadata" in me ? (me as any).user_metadata?.phone : null) || + null; + + if (rawPhone) phoneNumber = rawPhone; + } + + // 🔹 Normaliza para formato internacional (+55) + if (phoneNumber) { + phoneNumber = phoneNumber.replace(/\D/g, ""); + if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}`; + phoneNumber = `+${phoneNumber}`; + } + + console.log("📞 Telefone usado:", phoneNumber); +} catch (err) { + console.warn("⚠️ Não foi possível obter telefone do paciente:", err); +} - // padroniza número para formato internacional (+55) - if (phoneNumber) { - phoneNumber = phoneNumber.replace(/\D/g, ""); // remove caracteres não numéricos - if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}`; - phoneNumber = `+${phoneNumber}`; - } - } catch (err) { - console.warn("Não foi possível obter telefone do paciente:", err); - } // 💬 envia o SMS de confirmação // 💬 Envia o SMS de lembrete (sem mostrar nada ao paciente) +// 💬 Envia o SMS de lembrete (somente loga no console, não mostra no sistema) try { - const smsRes = await appointmentsService.send_sms({ + const smsRes = await smsService.sendSms({ phone_number: phoneNumber, message: `Lembrete: sua consulta é em ${dateFormatted} às ${selectedTime} na Clínica MediConnect.`, patient_id: patientId, }); if (smsRes?.success) { - console.log("✅ SMS enviado com sucesso:", smsRes.message_sid || smsRes.sid || "(sem SID retornado)"); + console.log("✅ SMS enviado com sucesso:", smsRes.message_sid); } else { console.warn("⚠️ Falha no envio do SMS:", smsRes); } @@ -287,6 +304,8 @@ try { } + + // 🧹 limpa os campos setSelectedDoctor(""); setSelectedDate(""); diff --git a/services/Sms.mjs b/services/Sms.mjs new file mode 100644 index 0000000..daf4984 --- /dev/null +++ b/services/Sms.mjs @@ -0,0 +1,58 @@ +/** + * Serviço de SMS via Supabase Edge Function (sem backend) + * Usa o token JWT salvo no localStorage (chave: "token") + */ + +const SUPABASE_FUNCTION_URL = + "https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/send-sms"; + +export const smsService = { + /** + * Envia um SMS de lembrete via Twilio + * @param {Object} params + * @param {string} params.phone_number - Ex: +5511999999999 + * @param {string} params.message - Mensagem de texto + * @param {string} [params.patient_id] - ID opcional do paciente + */ + async sendSms({ phone_number, message, patient_id }) { + try { + // 🔹 Busca o token salvo pelo login + const token = localStorage.getItem("token"); + + if (!token) { + console.error("❌ Nenhum token JWT encontrado no localStorage (chave: 'token')."); + return { success: false, error: "Token JWT não encontrado." }; + } + + const body = JSON.stringify({ + phone_number, + message, + patient_id, + }); + + console.log("[smsService] Enviando SMS para:", phone_number); + + const response = await fetch(SUPABASE_FUNCTION_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, // 🔑 autenticação Supabase + }, + body, + }); + + const result = await response.json(); + + if (!response.ok) { + console.error("❌ Falha no envio do SMS:", result); + return { success: false, error: result }; + } + + console.log("✅ SMS enviado com sucesso:", result); + return result; + } catch (err) { + console.error("❌ Erro inesperado ao enviar SMS:", err); + return { success: false, error: err.message }; + } + }, +}; diff --git a/services/api.mjs b/services/api.mjs index 573c1ff..d06bde5 100644 --- a/services/api.mjs +++ b/services/api.mjs @@ -89,11 +89,20 @@ async function request(endpoint, options = {}) { // --- CORREÇÃO 1: PARA O SUBMIT DO AGENDAMENTO --- // Se a resposta for um sucesso de criação (201) ou sem conteúdo (204), não quebra. - if (response.status === 201 || response.status === 204) { - return null; + // --- CORREÇÃO: funções do Supabase retornam 200 ou 201, nunca queremos perder o body --- + if (response.status === 204) { + return null; + } + + const text = await response.text(); + try { + return JSON.parse(text); + } catch { + return text || null; } - return response.json(); + + } // Exportamos o objeto 'api' com os métodos que os componentes vão usar. diff --git a/services/appointmentsApi.mjs b/services/appointmentsApi.mjs index bebf986..f5a5f7b 100644 --- a/services/appointmentsApi.mjs +++ b/services/appointmentsApi.mjs @@ -46,6 +46,6 @@ export const appointmentsService = { */ delete: (id) => api.delete(`/rest/v1/appointments?id=eq.${id}`), - send_sms: (data) => api.post("/functions/v1/send-sms", data) + }; \ No newline at end of file