medicos ordem alfabetica

This commit is contained in:
m1guelmcf 2025-11-26 11:14:25 -03:00
parent 83bdaed7aa
commit c41d561dd6

View File

@ -9,15 +9,20 @@ import { AvailabilityService } from "@/services/availabilityApi.mjs";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea"; 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 { 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");
@ -39,17 +44,32 @@ export default function ScheduleForm() {
const [tipoConsulta] = useState("presencial"); const [tipoConsulta] = useState("presencial");
const [duracao] = useState("30"); const [duracao] = useState("30");
const [disponibilidades, setDisponibilidades] = useState<any[]>([]); const [disponibilidades, setDisponibilidades] = useState<any[]>([]);
const [availabilityCounts, setAvailabilityCounts] = useState<Record<string, number>>({}); const [availabilityCounts, setAvailabilityCounts] = useState<
const [tooltip, setTooltip] = useState<{ x: number; y: number; text: string } | null>(null); Record<string, number>
>({});
const [tooltip, setTooltip] = useState<{
x: number;
y: number;
text: string;
} | null>(null);
const calendarRef = useRef<HTMLDivElement | null>(null); const calendarRef = useRef<HTMLDivElement | null>(null);
// Funções auxiliares // Funções auxiliares
const getWeekdayNumber = (weekday: string) => const getWeekdayNumber = (weekday: string) =>
["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"] [
.indexOf(weekday.toLowerCase()) + 1; "monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
].indexOf(weekday.toLowerCase()) + 1;
const getBrazilDate = (date: Date) => const getBrazilDate = (date: Date) =>
new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0)); new Date(
Date.UTC(date.getFullYear(), date.getMonth(), date.getDate(), 12, 0, 0)
);
// 🔹 Buscar dados do usuário e role // 🔹 Buscar dados do usuário e role
useEffect(() => { useEffect(() => {
@ -78,7 +98,10 @@ export default function ScheduleForm() {
setDoctors(data || []); setDoctors(data || []);
} catch (err) { } catch (err) {
console.error("Erro ao buscar médicos:", err); console.error("Erro ao buscar médicos:", err);
toast({ title: "Erro", description: "Não foi possível carregar médicos." }); toast({
title: "Erro",
description: "Não foi possível carregar médicos.",
});
} finally { } finally {
setLoadingDoctors(false); setLoadingDoctors(false);
} }
@ -101,7 +124,10 @@ export default function ScheduleForm() {
} }
}, []); }, []);
const computeAvailabilityCountsPreview = async (doctorId: string, dispList: any[]) => { const computeAvailabilityCountsPreview = async (
doctorId: string,
dispList: any[]
) => {
try { try {
const today = new Date(); const today = new Date();
const start = format(today, "yyyy-MM-dd"); const start = format(today, "yyyy-MM-dd");
@ -123,7 +149,9 @@ export default function ScheduleForm() {
const d = addDays(today, i); const d = addDays(today, i);
const key = format(d, "yyyy-MM-dd"); const key = format(d, "yyyy-MM-dd");
const dayOfWeek = d.getDay() === 0 ? 7 : d.getDay(); const dayOfWeek = d.getDay() === 0 ? 7 : d.getDay();
const dailyDisp = dispList.filter((p) => getWeekdayNumber(p.weekday) === dayOfWeek); const dailyDisp = dispList.filter(
(p) => getWeekdayNumber(p.weekday) === dayOfWeek
);
if (dailyDisp.length === 0) { if (dailyDisp.length === 0) {
counts[key] = 0; counts[key] = 0;
continue; continue;
@ -135,7 +163,8 @@ export default function ScheduleForm() {
const startMin = sh * 60 + sm; const startMin = sh * 60 + sm;
const endMin = eh * 60 + em; const endMin = eh * 60 + em;
const slot = p.slot_minutes || 30; const slot = p.slot_minutes || 30;
if (endMin >= startMin) possible += Math.floor((endMin - startMin) / slot) + 1; if (endMin >= startMin)
possible += Math.floor((endMin - startMin) / slot) + 1;
}); });
const occupied = apptsByDate[key] || 0; const occupied = apptsByDate[key] || 0;
counts[key] = Math.max(0, possible - occupied); counts[key] = Math.max(0, possible - occupied);
@ -161,166 +190,175 @@ export default function ScheduleForm() {
}, [selectedDoctor, loadDoctorDisponibilidades]); }, [selectedDoctor, loadDoctorDisponibilidades]);
// 🔹 Buscar horários disponíveis // 🔹 Buscar horários disponíveis
const fetchAvailableSlots = useCallback(async (doctorId: string, date: string) => { const fetchAvailableSlots = useCallback(
if (!doctorId || !date) return; async (doctorId: string, date: string) => {
setLoadingSlots(true); if (!doctorId || !date) return;
setAvailableTimes([]); setLoadingSlots(true);
try { setAvailableTimes([]);
const disponibilidades = await AvailabilityService.listById(doctorId); try {
const consultas = await appointmentsService.search_appointment( const disponibilidades = await AvailabilityService.listById(doctorId);
`doctor_id=eq.${doctorId}&scheduled_at=gte.${date}T00:00:00Z&scheduled_at=lt.${date}T23:59:59Z` const consultas = await appointmentsService.search_appointment(
); `doctor_id=eq.${doctorId}&scheduled_at=gte.${date}T00:00:00Z&scheduled_at=lt.${date}T23:59:59Z`
const diaJS = new Date(date).getDay(); );
const diaAPI = diaJS === 0 ? 7 : diaJS; const diaJS = new Date(date).getDay();
const disponibilidadeDia = disponibilidades.find( const diaAPI = diaJS === 0 ? 7 : diaJS;
(d: any) => getWeekdayNumber(d.weekday) === diaAPI const disponibilidadeDia = disponibilidades.find(
); (d: any) => getWeekdayNumber(d.weekday) === diaAPI
if (!disponibilidadeDia) { );
toast({ title: "Nenhuma disponibilidade", description: "Nenhum horário para este dia." }); if (!disponibilidadeDia) {
return setAvailableTimes([]); toast({
title: "Nenhuma disponibilidade",
description: "Nenhum horário para este dia.",
});
return setAvailableTimes([]);
}
const [startHour, startMin] = disponibilidadeDia.start_time
.split(":")
.map(Number);
const [endHour, endMin] = disponibilidadeDia.end_time
.split(":")
.map(Number);
const slot = disponibilidadeDia.slot_minutes || 30;
const horariosGerados: string[] = [];
let atual = new Date(date);
atual.setHours(startHour, startMin, 0, 0);
const end = new Date(date);
end.setHours(endHour, endMin, 0, 0);
while (atual <= end) {
horariosGerados.push(atual.toTimeString().slice(0, 5));
atual = new Date(atual.getTime() + slot * 60000);
}
const ocupados = (consultas || []).map((c: any) =>
String(c.scheduled_at).split("T")[1]?.slice(0, 5)
);
const livres = horariosGerados.filter((h) => !ocupados.includes(h));
setAvailableTimes(livres);
} catch (err) {
console.error(err);
toast({ title: "Erro", description: "Falha ao carregar horários." });
} finally {
setLoadingSlots(false);
} }
const [startHour, startMin] = disponibilidadeDia.start_time.split(":").map(Number); },
const [endHour, endMin] = disponibilidadeDia.end_time.split(":").map(Number); []
const slot = disponibilidadeDia.slot_minutes || 30; );
const horariosGerados: string[] = [];
let atual = new Date(date);
atual.setHours(startHour, startMin, 0, 0);
const end = new Date(date);
end.setHours(endHour, endMin, 0, 0);
while (atual <= end) {
horariosGerados.push(atual.toTimeString().slice(0, 5));
atual = new Date(atual.getTime() + slot * 60000);
}
const ocupados = (consultas || []).map((c: any) =>
String(c.scheduled_at).split("T")[1]?.slice(0, 5)
);
const livres = horariosGerados.filter((h) => !ocupados.includes(h));
setAvailableTimes(livres);
} catch (err) {
console.error(err);
toast({ title: "Erro", description: "Falha ao carregar horários." });
} finally {
setLoadingSlots(false);
}
}, []);
useEffect(() => { useEffect(() => {
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
// 🔹 Submeter agendamento // 🔹 Submeter agendamento
// 🔹 Submeter agendamento // 🔹 Submeter agendamento
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role); const isSecretaryLike = ["secretaria", "admin", "gestor"].includes(role);
const patientId = isSecretaryLike ? selectedPatient : userId; const patientId = isSecretaryLike ? selectedPatient : userId;
if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) { if (!patientId || !selectedDoctor || !selectedDate || !selectedTime) {
toast({ title: "Campos obrigatórios", description: "Preencha todos os campos." }); toast({
return; title: "Campos obrigatórios",
} description: "Preencha todos os campos.",
});
return;
}
try { try {
const body = { const body = {
doctor_id: selectedDoctor, doctor_id: selectedDoctor,
patient_id: patientId, patient_id: patientId,
scheduled_at: `${selectedDate}T${selectedTime}:00`, scheduled_at: `${selectedDate}T${selectedTime}:00`,
duration_minutes: Number(duracao), duration_minutes: Number(duracao),
notes, notes,
appointment_type: tipoConsulta, appointment_type: tipoConsulta,
}; };
// ✅ mantém o fluxo original de criação (funcional) // ✅ 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) ${
doctors.find((d) => d.id === selectedDoctor)?.full_name || "" doctors.find((d) => d.id === selectedDoctor)?.full_name || ""
}.`, }.`,
}); });
let phoneNumber = "+5511999999999"; // fallback let phoneNumber = "+5511999999999"; // fallback
try { try {
if (isSecretaryLike) { if (isSecretaryLike) {
// Secretária/admin → telefone do paciente selecionado // Secretária/admin → telefone do paciente selecionado
const patient = patients.find((p: any) => p.id === patientId); const patient = patients.find((p: any) => p.id === patientId);
// Pacientes criados no sistema podem ter phone ou phone_mobile // Pacientes criados no sistema podem ter phone ou phone_mobile
const rawPhone = patient?.phone || patient?.phone_mobile || null; const rawPhone = patient?.phone || patient?.phone_mobile || null;
if (rawPhone) phoneNumber = rawPhone; if (rawPhone) phoneNumber = rawPhone;
} else { } else {
// Paciente → telefone vem do perfil do próprio usuário logado // Paciente → telefone vem do perfil do próprio usuário logado
const me = await usersService.getMe(); 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;
const rawPhone = if (rawPhone) phoneNumber = 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, "");
// 🔹 Normaliza para formato internacional (+55) if (!phoneNumber.startsWith("55")) phoneNumber = `55${phoneNumber}`;
if (phoneNumber) { phoneNumber = `+${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("");
setSelectedDate("");
setSelectedTime("");
setNotes("");
setSelectedPatient("");
} catch (err) {
console.error("❌ Erro ao agendar consulta:", err);
toast({ title: "Erro", description: "Falha ao agendar consulta." });
}
};
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("");
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 // 🔹 Tooltip no calendário
useEffect(() => { useEffect(() => {
@ -359,19 +397,27 @@ try {
<CardTitle>Dados da Consulta</CardTitle> <CardTitle>Dados da Consulta</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleSubmit} className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <form
onSubmit={handleSubmit}
className="grid grid-cols-1 lg:grid-cols-2 gap-6"
>
<div className="space-y-3"> <div className="space-y-3">
{/* Se secretária/gestor/admin → mostrar campo Paciente */} {/* Se secretária/gestor/admin → mostrar campo Paciente */}
{["secretaria", "gestor", "admin"].includes(role) && ( {["secretaria", "gestor", "admin"].includes(role) && (
<div> <div>
<Label>Paciente</Label> <Label>Paciente</Label>
<Select value={selectedPatient} onValueChange={setSelectedPatient}> <Select
value={selectedPatient}
onValueChange={setSelectedPatient}
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione o paciente" /> <SelectValue placeholder="Selecione o paciente" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{patients.map((p) => ( {patients.map((p) => (
<SelectItem key={p.id} value={p.id}>{p.full_name}</SelectItem> <SelectItem key={p.id} value={p.id}>
{p.full_name}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@ -380,19 +426,28 @@ try {
<div> <div>
<Label>Médico</Label> <Label>Médico</Label>
<Select value={selectedDoctor} onValueChange={setSelectedDoctor}> <Select
value={selectedDoctor}
onValueChange={setSelectedDoctor}
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione o médico" /> <SelectValue placeholder="Selecione o médico" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{loadingDoctors ? ( {loadingDoctors ? (
<SelectItem value="loading" disabled>Carregando...</SelectItem> <SelectItem value="loading" disabled>
Carregando...
</SelectItem>
) : ( ) : (
doctors.map((d) => ( [...doctors]
<SelectItem key={d.id} value={d.id}> .sort((a, b) =>
{d.full_name} {d.specialty} String(a.full_name).localeCompare(String(b.full_name))
</SelectItem> )
)) .map((d) => (
<SelectItem key={d.id} value={d.id}>
{d.full_name} {d.specialty}
</SelectItem>
))
)} )}
</SelectContent> </SelectContent>
</Select> </Select>
@ -404,10 +459,17 @@ try {
<CalendarShadcn <CalendarShadcn
mode="single" mode="single"
disabled={!selectedDoctor} disabled={!selectedDoctor}
selected={selectedDate ? new Date(selectedDate + "T12:00:00") : undefined} selected={
selectedDate
? new Date(selectedDate + "T12:00:00")
: undefined
}
onSelect={(date) => { onSelect={(date) => {
if (!date) return; if (!date) return;
const formatted = format(new Date(date.getTime() + 12 * 60 * 60 * 1000), "yyyy-MM-dd"); const formatted = format(
new Date(date.getTime() + 12 * 60 * 60 * 1000),
"yyyy-MM-dd"
);
setSelectedDate(formatted); setSelectedDate(formatted);
}} }}
/> />
@ -436,7 +498,8 @@ try {
<User className="h-4 w-4 text-blue-600" /> <User className="h-4 w-4 text-blue-600" />
<div className="text-xs"> <div className="text-xs">
{selectedDoctor {selectedDoctor
? doctors.find((d) => d.id === selectedDoctor)?.full_name ? doctors.find((d) => d.id === selectedDoctor)
?.full_name
: "Médico"} : "Médico"}
</div> </div>
</div> </div>
@ -447,7 +510,10 @@ try {
<div className="space-y-2"> <div className="space-y-2">
<Label>Horário</Label> <Label>Horário</Label>
<Select onValueChange={setSelectedTime} disabled={loadingSlots || availableTimes.length === 0}> <Select
onValueChange={setSelectedTime}
disabled={loadingSlots || availableTimes.length === 0}
>
<SelectTrigger> <SelectTrigger>
<SelectValue <SelectValue
placeholder={ placeholder={
@ -461,7 +527,9 @@ try {
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{availableTimes.map((h) => ( {availableTimes.map((h) => (
<SelectItem key={h} value={h}>{h}</SelectItem> <SelectItem key={h} value={h}>
{h}
</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>