commit
74ad727ec4
@ -14,8 +14,10 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { Calendar as CalendarShadcn } from "@/components/ui/calendar";
|
import { Calendar as CalendarShadcn } from "@/components/ui/calendar";
|
||||||
import { format, addDays } from "date-fns";
|
import { format, addDays } from "date-fns";
|
||||||
import { User, StickyNote, Calendar } from "lucide-react";
|
import { User, StickyNote, Calendar } from "lucide-react";
|
||||||
|
import {smsService } from "@/services/Sms.mjs"
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
|
|
||||||
export default function ScheduleForm() {
|
export default function ScheduleForm() {
|
||||||
// Estado do usuário e role
|
// Estado do usuário e role
|
||||||
const [role, setRole] = useState<string>("paciente");
|
const [role, setRole] = useState<string>("paciente");
|
||||||
@ -206,6 +208,10 @@ export default function ScheduleForm() {
|
|||||||
if (selectedDoctor && selectedDate) fetchAvailableSlots(selectedDoctor, selectedDate);
|
if (selectedDoctor && selectedDate) fetchAvailableSlots(selectedDoctor, selectedDate);
|
||||||
}, [selectedDoctor, selectedDate, fetchAvailableSlots]);
|
}, [selectedDoctor, selectedDate, fetchAvailableSlots]);
|
||||||
|
|
||||||
|
// 🔹 Submeter agendamento
|
||||||
|
// 🔹 Submeter agendamento
|
||||||
|
// 🔹 Submeter agendamento
|
||||||
|
// 🔹 Submeter agendamento
|
||||||
// 🔹 Submeter agendamento
|
// 🔹 Submeter agendamento
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -228,8 +234,11 @@ export default function ScheduleForm() {
|
|||||||
appointment_type: tipoConsulta,
|
appointment_type: tipoConsulta,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ mantém o fluxo original de criação (funcional)
|
||||||
await appointmentsService.create(body);
|
await appointmentsService.create(body);
|
||||||
|
|
||||||
const dateFormatted = selectedDate.split("-").reverse().join("/");
|
const dateFormatted = selectedDate.split("-").reverse().join("/");
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: "Consulta agendada!",
|
title: "Consulta agendada!",
|
||||||
description: `Consulta marcada para ${dateFormatted} às ${selectedTime} com o(a) médico(a) ${
|
description: `Consulta marcada para ${dateFormatted} às ${selectedTime} com o(a) médico(a) ${
|
||||||
@ -237,17 +246,82 @@ export default function ScheduleForm() {
|
|||||||
}.`,
|
}.`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let phoneNumber = "+5511999999999"; // fallback
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 💬 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 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);
|
||||||
|
} else {
|
||||||
|
console.warn("⚠️ Falha no envio do SMS:", smsRes);
|
||||||
|
}
|
||||||
|
} catch (smsErr) {
|
||||||
|
console.error("❌ Erro ao enviar SMS:", smsErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 🧹 limpa os campos
|
||||||
setSelectedDoctor("");
|
setSelectedDoctor("");
|
||||||
setSelectedDate("");
|
setSelectedDate("");
|
||||||
setSelectedTime("");
|
setSelectedTime("");
|
||||||
setNotes("");
|
setNotes("");
|
||||||
setSelectedPatient("");
|
setSelectedPatient("");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error("❌ Erro ao agendar consulta:", err);
|
||||||
toast({ title: "Erro", description: "Falha ao agendar consulta." });
|
toast({ title: "Erro", description: "Falha ao agendar consulta." });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 🔹 Tooltip no calendário
|
// 🔹 Tooltip no calendário
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const cont = calendarRef.current;
|
const cont = calendarRef.current;
|
||||||
|
|||||||
58
services/Sms.mjs
Normal file
58
services/Sms.mjs
Normal file
@ -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 };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -89,11 +89,20 @@ async function request(endpoint, options = {}) {
|
|||||||
|
|
||||||
// --- CORREÇÃO 1: PARA O SUBMIT DO AGENDAMENTO ---
|
// --- 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.
|
// 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) {
|
// --- CORREÇÃO: funções do Supabase retornam 200 ou 201, nunca queremos perder o body ---
|
||||||
|
if (response.status === 204) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.json();
|
const text = await response.text();
|
||||||
|
try {
|
||||||
|
return JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
return text || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exportamos o objeto 'api' com os métodos que os componentes vão usar.
|
// Exportamos o objeto 'api' com os métodos que os componentes vão usar.
|
||||||
|
|||||||
@ -45,4 +45,7 @@ export const appointmentsService = {
|
|||||||
* @returns {Promise<object>} - Uma promessa que resolve com a resposta da API.
|
* @returns {Promise<object>} - Uma promessa que resolve com a resposta da API.
|
||||||
*/
|
*/
|
||||||
delete: (id) => api.delete(`/rest/v1/appointments?id=eq.${id}`),
|
delete: (id) => api.delete(`/rest/v1/appointments?id=eq.${id}`),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user