From 6846a30f66ab25dec8437027c8e60da59cc83117 Mon Sep 17 00:00:00 2001 From: GagoDuBroca Date: Tue, 14 Oct 2025 10:18:06 -0300 Subject: [PATCH 1/9] disponibilidade --- app/doctor/disponibilidade/page.tsx | 130 ++++++++++++++++++++++++++++ components/doctor-layout.tsx | 8 +- services/availabilityApi.mjs | 8 ++ 3 files changed, 145 insertions(+), 1 deletion(-) create mode 100644 app/doctor/disponibilidade/page.tsx create mode 100644 services/availabilityApi.mjs diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx new file mode 100644 index 0000000..0b4c021 --- /dev/null +++ b/app/doctor/disponibilidade/page.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import DoctorLayout from "@/components/doctor-layout"; +import {AvailabilityService} from "@/services/availabilityApi.mjs" + +export default function AvailabilityPage() { + const [error, setError] = useState(null); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await AvailabilityService.list(); + console.log(response); + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } + }; + + fetchData(); +}, []); + return ( + +
+
+
+

Definir Disponibilidade

+

Defina sua disponibilidade para consultas

+
+
+ +
+
+

Dados

+ +
+
+
+ +
+ + + + + + + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ +
+ + + + +
+
+
+
+ ); +} diff --git a/components/doctor-layout.tsx b/components/doctor-layout.tsx index 27d5cdc..42a3df6 100644 --- a/components/doctor-layout.tsx +++ b/components/doctor-layout.tsx @@ -103,7 +103,7 @@ useEffect(() => { const menuItems = [ { - href: "#", + href: "/doctor/dashboard", icon: Home, label: "Dashboard", // Botão para o dashboard do médico @@ -126,6 +126,12 @@ useEffect(() => { label: "Pacientes", // Botão para a página de visualização de todos os pacientes }, + { + href: "/doctor/disponibilidade", + icon: Calendar, + label: "Disponibilidade", + // Botão para o dashboard do médico + }, ]; if (!doctorData) { diff --git a/services/availabilityApi.mjs b/services/availabilityApi.mjs new file mode 100644 index 0000000..71be51e --- /dev/null +++ b/services/availabilityApi.mjs @@ -0,0 +1,8 @@ +import { api } from "./api.mjs"; + +export const AvailabilityService = { + list: () => api.get("/rest/v1/doctor_availability"), + create: (data) => api.post("/rest/v1/doctor_availability", data), + update: (id, data) => api.patch(`/rest/v1/doctor_availability?id=eq.${id}`, data), + delete: (id) => api.delete(`/rest/v1/doctor_availability/{id}?id=eq.${id}`), +}; \ No newline at end of file From 1538d37e512f86b28df68fd98f30509e00a10a7c Mon Sep 17 00:00:00 2001 From: StsDanilo Date: Tue, 14 Oct 2025 20:57:33 -0300 Subject: [PATCH 2/9] =?UTF-8?q?adicionado=20hor=C3=A1rio=20semanal=20ao=20?= =?UTF-8?q?dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/doctor/dashboard/page.tsx | 126 ++++++++++++++++++++++++++-- app/doctor/disponibilidade/page.tsx | 118 ++++++++++++++++++-------- 2 files changed, 205 insertions(+), 39 deletions(-) diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index d9615f2..5d90d6b 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -1,10 +1,91 @@ -import DoctorLayout from "@/components/doctor-layout" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Calendar, Clock, User, Plus } from "lucide-react" -import Link from "next/link" +"use client"; + +import DoctorLayout from "@/components/doctor-layout"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Calendar, Clock, User, Plus } from "lucide-react"; +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { AvailabilityService } from "@/services/availabilityApi.mjs"; + +type Availability = { + id: string; + doctor_id: string; + weekday: string; + start_time: string; + end_time: string; + slot_minutes: number; + appointment_type: string; + active: boolean; + created_at: string; + updated_at: string; + created_by: string; + updated_by: string | null; +}; + +type Schedule = { + weekday: object; +}; export default function PatientDashboard() { + const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + const doctorId = "58ea5330-5cfe-4433-a218-2749844aee89"; //userInfo.id; + const [availability, setAvailability] = useState(null); + const [schedule, setSchedule] = useState>({}); + const formatTime = (time: string) => time.split(":").slice(0, 2).join(":"); + // Mapa de tradução + const weekdaysPT: Record = { + sunday: "Domingo", + monday: "Segunda", + tuesday: "Terça", + wednesday: "Quarta", + thursday: "Quinta", + friday: "Sexta", + saturday: "Sábado", + }; + + useEffect(() => { + const fetchData = async () => { + try { + const response = await AvailabilityService.list(); + const filteredResponse = response.filter((disp: { doctor_id: any }) => disp.doctor_id == doctorId); + setAvailability(filteredResponse); + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } + }; + fetchData(); + }, []); + + function formatAvailability(data: Availability[]) { + // Agrupar os horários por dia da semana + const schedule = data.reduce((acc: any, item) => { + const { weekday, start_time, end_time } = item; + + // Se o dia ainda não existe, cria o array + if (!acc[weekday]) { + acc[weekday] = []; + } + + // Adiciona o horário do dia + acc[weekday].push({ + start: start_time, + end: end_time, + }); + + return acc; + }, {} as Record); + + return schedule; + } + + useEffect(() => { + if (availability) { + const formatted = formatAvailability(availability); + setSchedule(formatted); + } + }, [availability]); + return (
@@ -85,7 +166,40 @@ export default function PatientDashboard() {
+
+ + + Horário Semanal + Confira rapidamente a sua disponibilidade da semana + + + {["sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"].map((day) => { + const times = schedule[day] || []; + return ( +
+
+
+

{weekdaysPT[day]}

+
+
+ {times.length > 0 ? ( + times.map((t, i) => ( +

+ {formatTime(t.start)}
{formatTime(t.end)} +

+ )) + ) : ( +

Sem horário

+ )} +
+
+
+ ); + })} +
+
+
- ) + ); } diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index 0b4c021..55c3cac 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -7,23 +7,77 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import DoctorLayout from "@/components/doctor-layout"; -import {AvailabilityService} from "@/services/availabilityApi.mjs" +import { AvailabilityService } from "@/services/availabilityApi.mjs"; +import { toast } from "@/hooks/use-toast"; +import { useRouter } from "next/navigation"; export default function AvailabilityPage() { - const [error, setError] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await AvailabilityService.list(); - console.log(response); - } catch (e: any) { - alert(`${e?.error} ${e?.message}`); - } - }; + const [error, setError] = useState(null); + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + const [modalidadeConsulta, setModalidadeConsulta] = useState(""); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await AvailabilityService.list(); + console.log(response); + } catch (e: any) { + alert(`${e?.error} ${e?.message}`); + } + }; + + fetchData(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (isLoading) return; + setIsLoading(true); + const form = e.currentTarget; + const formData = new FormData(form); + + const apiPayload = { + doctor_id: userInfo.id, + created_by: userInfo.id, + weekday: (formData.get("weekday") as string) || undefined, + start_time: (formData.get("horarioEntrada") as string) || undefined, + end_time: (formData.get("horarioSaida") as string) || undefined, + slot_minutes: Number(formData.get("duracaoConsulta")) || undefined, + appointment_type: modalidadeConsulta || undefined, + active: true, + }; + console.log(apiPayload); + + try { + const res = await AvailabilityService.create(apiPayload); + console.log(res); + + let message = "disponibilidade cadastrada com sucesso"; + try { + if (!res[0].id) { + throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); + } else { + console.log(message); + } + } catch {} + + toast({ + title: "Sucesso", + description: message, + }); + router.push("#"); // adicionar página para listar a disponibilidade + } catch (err: any) { + toast({ + title: "Erro", + description: err?.message || "Não foi possível cadastrar o paciente", + }); + } finally { + setIsLoading(false); + } + }; - fetchData(); -}, []); return (
@@ -34,65 +88,65 @@ export default function AvailabilityPage() {
-
+

Dados

-
+
- -
+ +
- +
- +
- +
@@ -100,18 +154,16 @@ export default function AvailabilityPage() { - setModalidadeConsulta(value)} value={modalidadeConsulta}> Presencial Telemedicina -
-
@@ -120,7 +172,7 @@ export default function AvailabilityPage() {
From 88f8954dd3f27e63d636597df0eb72661b35b297 Mon Sep 17 00:00:00 2001 From: StsDanilo Date: Wed, 15 Oct 2025 19:12:12 -0300 Subject: [PATCH 3/9] =?UTF-8?q?Adicionado=20cria=C3=A7=C3=A3o=20de=20dispo?= =?UTF-8?q?nibilidade=20e=20exce=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/doctor/dashboard/page.tsx | 114 +++++++++- app/doctor/disponibilidade/excecoes/page.tsx | 219 +++++++++++++++++++ app/doctor/disponibilidade/page.tsx | 12 +- services/exceptionApi.mjs | 7 + 4 files changed, 346 insertions(+), 6 deletions(-) create mode 100644 app/doctor/disponibilidade/excecoes/page.tsx create mode 100644 services/exceptionApi.mjs diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index 5d90d6b..c8a6c2c 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -3,10 +3,13 @@ import DoctorLayout from "@/components/doctor-layout"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { Calendar, Clock, User, Plus } from "lucide-react"; +import { Calendar, Clock, User, Trash2 } from "lucide-react"; import Link from "next/link"; import { useEffect, useState } from "react"; +import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { AvailabilityService } from "@/services/availabilityApi.mjs"; +import { exceptionsService } from "@/services/exceptionApi.mjs"; +import { toast } from "@/hooks/use-toast"; type Availability = { id: string; @@ -29,10 +32,15 @@ type Schedule = { export default function PatientDashboard() { const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); - const doctorId = "58ea5330-5cfe-4433-a218-2749844aee89"; //userInfo.id; + const doctorId = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; //userInfo.id; const [availability, setAvailability] = useState(null); + const [exceptions, setExceptions] = useState(null); const [schedule, setSchedule] = useState>({}); const formatTime = (time: string) => time.split(":").slice(0, 2).join(":"); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [patientToDelete, setPatientToDelete] = useState(null); + const [error, setError] = useState(null); + // Mapa de tradução const weekdaysPT: Record = { sunday: "Domingo", @@ -47,9 +55,14 @@ export default function PatientDashboard() { useEffect(() => { const fetchData = async () => { try { + // fetch para disponibilidade const response = await AvailabilityService.list(); const filteredResponse = response.filter((disp: { doctor_id: any }) => disp.doctor_id == doctorId); setAvailability(filteredResponse); + // fetch para exceções + const res = await exceptionsService.list(); + const filteredRes = res.filter((disp: { doctor_id: any }) => disp.doctor_id == doctorId); + setExceptions(filteredRes); } catch (e: any) { alert(`${e?.error} ${e?.message}`); } @@ -57,6 +70,41 @@ export default function PatientDashboard() { fetchData(); }, []); + const openDeleteDialog = (patientId: string) => { + setPatientToDelete(patientId); + setDeleteDialogOpen(true); + }; + + const handleDeletePatient = async (patientId: string) => { + // Remove from current list (client-side deletion) + try { + const res = await exceptionsService.delete(patientId); + + let message = "Exceção deletada com sucesso"; + try { + if (res) { + throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); + } else { + console.log(message); + } + } catch {} + + toast({ + title: "Sucesso", + description: message, + }); + + setExceptions((prev: any[]) => prev.filter((p) => String(p.id) !== String(patientId))); + } catch (e: any) { + toast({ + title: "Erro", + description: e?.message || "Não foi possível deletar a exceção", + }); + } + setDeleteDialogOpen(false); + setPatientToDelete(null); + }; + function formatAvailability(data: Availability[]) { // Agrupar os horários por dia da semana const schedule = data.reduce((acc: any, item) => { @@ -199,6 +247,68 @@ export default function PatientDashboard() { +
+ + + Exceções + Bloqueios e liberações eventuais de agenda + + + + {exceptions && exceptions.length > 0 ? ( + exceptions.map((ex: any) => { + // Formata data e hora + const date = new Date(ex.date).toLocaleDateString("pt-BR", { + weekday: "long", + day: "2-digit", + month: "long", + }); + + const startTime = formatTime(ex.start_time); + const endTime = formatTime(ex.end_time); + + return ( +
+
+
+

{date}

+

+ {startTime} - {endTime} +

+
+
+

{ex.kind === "bloqueio" ? "Bloqueio" : "Liberação"}

+

{ex.reason || "Sem motivo especificado"}

+
+
+ +
+
+
+ ); + }) + ) : ( +

Nenhuma exceção registrada.

+ )} +
+
+
+ + + + Confirmar exclusão + Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita. + + + Cancelar + patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700"> + Excluir + + + +
); diff --git a/app/doctor/disponibilidade/excecoes/page.tsx b/app/doctor/disponibilidade/excecoes/page.tsx new file mode 100644 index 0000000..6eafdde --- /dev/null +++ b/app/doctor/disponibilidade/excecoes/page.tsx @@ -0,0 +1,219 @@ +"use client"; + +import type React from "react"; +import Link from "next/link"; +import { useState, useEffect } from "react"; +import DoctorLayout from "@/components/doctor-layout"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Clock, Calendar as CalendarIcon, MapPin, Phone, User, X, RefreshCw } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { useRouter } from "next/navigation"; +import { toast } from "@/hooks/use-toast"; +import { exceptionsService } from "@/services/exceptionApi.mjs"; + +// IMPORTAR O COMPONENTE CALENDÁRIO DA SHADCN +import { Calendar } from "@/components/ui/calendar"; +import { format } from "date-fns"; // Usaremos o date-fns para formatação e comparação de datas + +const APPOINTMENTS_STORAGE_KEY = "clinic-appointments"; + +// --- TIPAGEM DA CONSULTA SALVA NO LOCALSTORAGE --- +interface LocalStorageAppointment { + id: number; + patientName: string; + doctor: string; + specialty: string; + date: string; // Data no formato YYYY-MM-DD + time: string; // Hora no formato HH:MM + status: "agendada" | "confirmada" | "cancelada" | "realizada"; + location: string; + phone: string; +} + +const LOGGED_IN_DOCTOR_NAME = "Dr. João Santos"; + +// Função auxiliar para comparar se duas datas (Date objects) são o mesmo dia +const isSameDay = (date1: Date, date2: Date) => { + return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate(); +}; + +// --- COMPONENTE PRINCIPAL --- + +export default function ExceptionPage() { + const [allAppointments, setAllAppointments] = useState([]); + const router = useRouter(); + const [filteredAppointments, setFilteredAppointments] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; + const [tipo, setTipo] = useState(""); + + // NOVO ESTADO 1: Armazena os dias com consultas (para o calendário) + const [bookedDays, setBookedDays] = useState([]); + + // NOVO ESTADO 2: Armazena a data selecionada no calendário + const [selectedCalendarDate, setSelectedCalendarDate] = useState(new Date()); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (isLoading) return; + //setIsLoading(true); + const form = e.currentTarget; + const formData = new FormData(form); + + const apiPayload = { + doctor_id: doctorIdTemp, + created_by: doctorIdTemp, + date: selectedCalendarDate ? format(selectedCalendarDate, "yyyy-MM-dd") : "", + start_time: ((formData.get("horarioEntrada") + ":00") as string) || undefined, + end_time: ((formData.get("horarioSaida") + ":00") as string) || undefined, + kind: tipo || undefined, + reason: formData.get("reason"), + }; + console.log(apiPayload); + try { + const res = await exceptionsService.create(apiPayload); + console.log(res); + + let message = "Exceção cadastrada com sucesso"; + try { + if (!res[0].id) { + throw new Error(`${res.error} ${res.message}` || "A API retornou erro"); + } else { + console.log(message); + } + } catch {} + + toast({ + title: "Sucesso", + description: message, + }); + router.push("/doctor/dashboard"); // adicionar página para listar a disponibilidade + } catch (err: any) { + toast({ + title: "Erro", + description: err?.message || "Não foi possível cadastrar a exceção", + }); + } finally { + setIsLoading(false); + } + }; + + const displayDate = selectedCalendarDate ? new Date(selectedCalendarDate).toLocaleDateString("pt-BR", { weekday: "long", day: "2-digit", month: "long" }) : "Selecione uma data"; + + return ( + +
+
+

Adicione exceções

+

Altere a disponibilidade em casos especiais para o Dr. {userInfo.user_metadata.full_name}

+
+ +
+

Consultas para: {displayDate}

+ +
+ + {/* NOVO LAYOUT DE DUAS COLUNAS */} +
+ {/* COLUNA 1: CALENDÁRIO */} +
+ + + + + Calendário + +

Selecione a data desejada.

+
+ + + +
+
+ + {/* COLUNA 2: FORM PARA ADICIONAR EXCEÇÃO */} +
+ {isLoading ? ( +

Carregando a agenda...

+ ) : !selectedCalendarDate ? ( +

Selecione uma data.

+ ) : ( +
+
+

Dados

+

{selectedCalendarDate?.toLocaleDateString("pt-BR", { weekday: "long" })}

+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+
+ +
+ + + + +
+
+ )} +
+
+
+
+ ); +} diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index 55c3cac..4a2beb0 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -16,6 +16,7 @@ export default function AvailabilityPage() { const router = useRouter(); const [isLoading, setIsLoading] = useState(false); const userInfo = JSON.parse(localStorage.getItem("user_info") || "{}"); + const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225"; const [modalidadeConsulta, setModalidadeConsulta] = useState(""); useEffect(() => { @@ -39,8 +40,8 @@ export default function AvailabilityPage() { const formData = new FormData(form); const apiPayload = { - doctor_id: userInfo.id, - created_by: userInfo.id, + doctor_id: doctorIdTemp, + created_by: doctorIdTemp, weekday: (formData.get("weekday") as string) || undefined, start_time: (formData.get("horarioEntrada") as string) || undefined, end_time: (formData.get("horarioSaida") as string) || undefined, @@ -159,7 +160,7 @@ export default function AvailabilityPage() { - Presencial + Presencial Telemedicina @@ -168,7 +169,10 @@ export default function AvailabilityPage() {
- + + + + - - - - + + + + + + + + {/* Card — Gestão de Médicos */} Gestão de Médicos - Médicos online + Médicos cadastrados recentemente -
-
-
-

Dr. Silva

-

Cardiologia

-
-
-

On-line

-

-
+ {loadingDoctors ? ( +

Carregando médicos...

+ ) : doctors.length === 0 ? ( +

Nenhum médico cadastrado.

+ ) : ( +
+ {doctors.map((doc, index) => ( +
+
+

{doc.full_name || "Sem nome"}

+

+ {doc.specialty || "Sem especialidade"} +

+
+
+

+ {doc.active ? "Ativo" : "Inativo"} +

+
+
+ ))}
-
-
-

Dra. Santos

-

Dermatologia

-
-
-

Off-line

-

Visto as 8:33

-
-
-
+ )}
- ) + ); } diff --git a/app/secretary/dashboard/page.tsx b/app/secretary/dashboard/page.tsx index 7171aa8..e37141c 100644 --- a/app/secretary/dashboard/page.tsx +++ b/app/secretary/dashboard/page.tsx @@ -1,41 +1,207 @@ -import SecretaryLayout from "@/components/secretary-layout" -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" -import { Button } from "@/components/ui/button" -import { Calendar, Clock, User, Plus } from "lucide-react" -import Link from "next/link" +"use client"; + +import SecretaryLayout from "@/components/secretary-layout"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Calendar, Clock, User, Plus } from "lucide-react"; +import Link from "next/link"; +import React, { useState, useEffect } from "react"; +import { patientsService } from "@/services/patientsApi.mjs"; +import { appointmentsService } from "@/services/appointmentsApi.mjs"; export default function SecretaryDashboard() { + // Estados + const [patients, setPatients] = useState([]); + const [loadingPatients, setLoadingPatients] = useState(true); + + const [firstConfirmed, setFirstConfirmed] = useState(null); + const [nextAgendada, setNextAgendada] = useState(null); + const [loadingAppointments, setLoadingAppointments] = useState(true); + + // 🔹 Buscar pacientes + useEffect(() => { + async function fetchPatients() { + try { + const data = await patientsService.list(); + if (Array.isArray(data)) { + setPatients(data.slice(0, 3)); + } + } catch (error) { + console.error("Erro ao carregar pacientes:", error); + } finally { + setLoadingPatients(false); + } + } + fetchPatients(); + }, []); + + // 🔹 Buscar consultas (confirmadas + 1ª do mês) + useEffect(() => { + async function fetchAppointments() { + try { + const hoje = new Date(); + const inicioMes = new Date(hoje.getFullYear(), hoje.getMonth(), 1); + const fimMes = new Date(hoje.getFullYear(), hoje.getMonth() + 1, 0); + + // Mesmo parâmetro de ordenação da página /secretary/appointments + const queryParams = "order=scheduled_at.desc"; + const data = await appointmentsService.search_appointment(queryParams); + + if (!Array.isArray(data) || data.length === 0) { + setFirstConfirmed(null); + setNextAgendada(null); + return; + } + + // 🩵 1️⃣ Consultas confirmadas (para o card “Próxima Consulta Confirmada”) + const confirmadas = data.filter((apt: any) => { + const dataConsulta = new Date(apt.scheduled_at || apt.date); + return apt.status === "confirmed" && dataConsulta >= hoje; + }); + + confirmadas.sort( + (a: any, b: any) => + new Date(a.scheduled_at || a.date).getTime() - + new Date(b.scheduled_at || b.date).getTime() + ); + + setFirstConfirmed(confirmadas[0] || null); + + // 💙 2️⃣ Consultas deste mês — pegar sempre a 1ª (mais próxima) + const consultasMes = data.filter((apt: any) => { + const dataConsulta = new Date(apt.scheduled_at); + return dataConsulta >= inicioMes && dataConsulta <= fimMes; + }); + + if (consultasMes.length > 0) { + consultasMes.sort( + (a: any, b: any) => + new Date(a.scheduled_at).getTime() - + new Date(b.scheduled_at).getTime() + ); + setNextAgendada(consultasMes[0]); + } else { + setNextAgendada(null); + } + } catch (error) { + console.error("Erro ao carregar consultas:", error); + } finally { + setLoadingAppointments(false); + } + } + + fetchAppointments(); + }, []); + return (
+ {/* Cabeçalho */}

Dashboard

Bem-vindo ao seu portal de consultas médicas

+ {/* Cards principais */}
+ {/* Próxima Consulta Confirmada */} - Próxima Consulta + + Próxima Consulta Confirmada + -
15 Jan
-

Dr. Silva - 14:30

+ {loadingAppointments ? ( +
+ Carregando próxima consulta... +
+ ) : firstConfirmed ? ( + <> +
+ {new Date( + firstConfirmed.scheduled_at || firstConfirmed.date + ).toLocaleDateString("pt-BR")} +
+

+ {firstConfirmed.doctor_name + ? `Dr(a). ${firstConfirmed.doctor_name}` + : "Médico não informado"}{" "} + -{" "} + {new Date( + firstConfirmed.scheduled_at + ).toLocaleTimeString("pt-BR", { + hour: "2-digit", + minute: "2-digit", + })} +

+ + ) : ( +
+ Nenhuma consulta confirmada encontrada +
+ )}
+ {/* Consultas Este Mês */} - Consultas Este Mês + + Consultas Este Mês + -
3
-

2 realizadas, 1 agendada

+ {loadingAppointments ? ( +
+ Carregando consultas... +
+ ) : nextAgendada ? ( + <> +
+ {new Date( + nextAgendada.scheduled_at + ).toLocaleDateString("pt-BR", { + day: "2-digit", + month: "2-digit", + year: "numeric", + })}{" "} + às{" "} + {new Date( + nextAgendada.scheduled_at + ).toLocaleTimeString("pt-BR", { + hour: "2-digit", + minute: "2-digit", + })} +
+

+ {nextAgendada.doctor_name + ? `Dr(a). ${nextAgendada.doctor_name}` + : "Médico não informado"} +

+

+ {nextAgendada.patient_name + ? `Paciente: ${nextAgendada.patient_name}` + : ""} +

+ + ) : ( +
+ Nenhuma consulta agendada neste mês +
+ )}
+ {/* Perfil */} Perfil @@ -48,11 +214,15 @@ export default function SecretaryDashboard() {
+ {/* Cards Secundários */}
+ {/* Ações rápidas */} Ações Rápidas - Acesse rapidamente as principais funcionalidades + + Acesse rapidamente as principais funcionalidades + @@ -62,52 +232,73 @@ export default function SecretaryDashboard() { - - - + {/* Pacientes */} - Próximas Consultas - Suas consultas agendadas + Pacientes + + Últimos pacientes cadastrados + -
-
-
-

Dr. Silva

-

Cardiologia

-
-
-

15 Jan

-

14:30

-
+ {loadingPatients ? ( +

+ Carregando pacientes... +

+ ) : patients.length === 0 ? ( +

+ Nenhum paciente cadastrado. +

+ ) : ( +
+ {patients.map((patient, index) => ( +
+
+

+ {patient.full_name || "Sem nome"} +

+

+ {patient.phone_mobile || + patient.phone1 || + "Sem telefone"} +

+
+
+

+ {patient.convenio || "Particular"} +

+
+
+ ))}
-
-
-

Dra. Santos

-

Dermatologia

-
-
-

22 Jan

-

10:00

-
-
-
+ )}
- ) + ); } From f6f206ff63af5860a4f8f317c60dadc74aea6763 Mon Sep 17 00:00:00 2001 From: Gabriel Lira Figueira Date: Wed, 15 Oct 2025 23:29:31 -0300 Subject: [PATCH 5/9] =?UTF-8?q?refactor(auth):=20Centraliza=20e=20padroniz?= =?UTF-8?q?a=20o=20fluxo=20de=20autentica=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Esta refatoração unifica todo o sistema de login e logout da aplicação, resolvendo inconsistências e eliminando código duplicado. Problema Anterior: - A lógica de login estava espalhada por múltiplos componentes e páginas (`/doctor/login`, `/patient/login`, etc.). - Cada layout de área restrita (`DoctorLayout`, `PatientLayout`, etc.) tinha sua própria lógica de verificação de segurança e logout, resultando em bugs (ex: uso de Cookies vs. localStorage). Solução Aplicada: - Foi criado um componente `LoginForm` unificado e inteligente, responsável por toda a interação de login. - Toda a lógica de comunicação com a API de autenticação foi centralizada no serviço `api.mjs`, incluindo uma nova função `api.logout()`. - Todos os layouts de áreas restritas (`DoctorLayout`, `PatientLayout`, etc.) foram padronizados para usar `localStorage.getItem('token')` para verificação e para chamar `api.logout()` ao sair. - As páginas de login específicas de cada perfil foram atualizadas para usar o novo `LoginForm` genérico. --- app/doctor/login/page.tsx | 24 ++- app/finance/login/page.tsx | 25 ++- app/login/page.tsx | 82 ++++++++ app/manager/login/page.tsx | 25 ++- app/page.tsx | 4 +- app/patient/login/page.tsx | 31 ++- app/secretary/login/page.tsx | 26 ++- components/LoginForm.tsx | 340 +++++++++++++------------------- components/doctor-layout.tsx | 107 +++++----- components/finance-layout.tsx | 121 +++++------- components/manager-layout.tsx | 180 +++++------------ components/patient-layout.tsx | 75 +++---- components/secretary-layout.tsx | 104 +++++----- services/api.mjs | 88 +++++---- 14 files changed, 615 insertions(+), 617 deletions(-) create mode 100644 app/login/page.tsx diff --git a/app/doctor/login/page.tsx b/app/doctor/login/page.tsx index c9cf646..051706e 100644 --- a/app/doctor/login/page.tsx +++ b/app/doctor/login/page.tsx @@ -1,11 +1,31 @@ // Caminho: app/(doctor)/login/page.tsx import { LoginForm } from "@/components/LoginForm"; +import Link from "next/link"; // Adicionado para o link de "Voltar" export default function DoctorLoginPage() { + // NOTA: Esta página se tornou obsoleta com a criação do /login central. + // O ideal no futuro é deletar esta página e redirecionar os usuários. + return (
- +
+

Área do Médico

+

Acesse o sistema médico

+ + {/* --- ALTERAÇÃO PRINCIPAL AQUI --- */} + {/* Chamando o LoginForm unificado sem props desnecessárias */} + + {/* Adicionamos um link de "Voltar" como filho (children) */} +
+ + + Voltar à página inicial + + +
+
+
); -} +} \ No newline at end of file diff --git a/app/finance/login/page.tsx b/app/finance/login/page.tsx index d00e41c..636680a 100644 --- a/app/finance/login/page.tsx +++ b/app/finance/login/page.tsx @@ -1,12 +1,31 @@ // Caminho: app/(finance)/login/page.tsx import { LoginForm } from "@/components/LoginForm"; +import Link from "next/link"; // Adicionado para o link de "Voltar" export default function FinanceLoginPage() { + // NOTA: Esta página se tornou obsoleta com a criação do /login central. + // O ideal no futuro é deletar esta página e redirecionar os usuários. + return ( - // Fundo com gradiente laranja, como no seu código original
- +
+

Área Financeira

+

Acesse o sistema de faturamento

+ + {/* --- ALTERAÇÃO PRINCIPAL AQUI --- */} + {/* Chamando o LoginForm unificado sem props desnecessárias */} + + {/* Adicionamos um link de "Voltar" como filho (children) */} +
+ + + Voltar à página inicial + + +
+
+
); -} +} \ No newline at end of file diff --git a/app/login/page.tsx b/app/login/page.tsx new file mode 100644 index 0000000..b193142 --- /dev/null +++ b/app/login/page.tsx @@ -0,0 +1,82 @@ +// Caminho: app/login/page.tsx + +import { LoginForm } from "@/components/LoginForm"; +import Link from "next/link"; +import Image from "next/image"; +import { Button } from "@/components/ui/button"; +import { ArrowLeft } from "lucide-react"; // Importa o ícone de seta + +export default function LoginPage() { + return ( +
+ + {/* PAINEL ESQUERDO: O Formulário */} +
+ + {/* Link para Voltar */} +
+ + + Voltar à página inicial + +
+ + {/* O contêiner principal que agora terá a sombra e o estilo de card */} +
+
+

Acesse sua conta

+

Bem-vindo(a) de volta ao MedConnect!

+
+ + + {/* Children para o LoginForm */} +
+ + + Esqueceu sua senha? + + +
+
+ +
+ Não tem uma conta de paciente? + + + Crie uma agora + + +
+
+
+ + {/* PAINEL DIREITO: A Imagem e Branding */} +
+ {/* Usamos o componente para otimização e performance */} + Médica utilizando um tablet na clínica MedConnect + {/* Camada de sobreposição para escurecer a imagem e destacar o texto */} +
+ {/* BLOCO DE NOME ADICIONADO */} +
+

+ MedConnect +

+
+

+ Tecnologia e Cuidado a Serviço da Sua Saúde. +

+

+ Acesse seu portal para uma experiência de saúde integrada, segura e eficiente. +

+
+
+ +
+ ); +} \ No newline at end of file diff --git a/app/manager/login/page.tsx b/app/manager/login/page.tsx index db3aadc..cb236d3 100644 --- a/app/manager/login/page.tsx +++ b/app/manager/login/page.tsx @@ -1,12 +1,31 @@ // Caminho: app/(manager)/login/page.tsx import { LoginForm } from "@/components/LoginForm"; +import Link from "next/link"; // Adicionado para o link de "Voltar" export default function ManagerLoginPage() { + // NOTA: Esta página se tornou obsoleta com a criação do /login central. + // O ideal no futuro é deletar esta página e redirecionar os usuários. + return ( - // Mantemos o seu plano de fundo original
- +
+

Área do Gestor

+

Acesse o sistema médico

+ + {/* --- ALTERAÇÃO PRINCIPAL AQUI --- */} + {/* Chamando o LoginForm unificado sem props desnecessárias */} + + {/* Adicionamos um link de "Voltar" como filho (children) */} +
+ + + Voltar à página inicial + + +
+
+
); -} +} \ No newline at end of file diff --git a/app/page.tsx b/app/page.tsx index b254689..5c3d1f6 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -17,7 +17,7 @@ export default function InicialPage() {

MedConnect

{} - + - +
+ Não tem uma conta? + + + Crie uma agora + + +
- {/* Conteúdo e espaçamento restaurados */}

Problemas para acessar? Entre em contato conosco

-
+ ); -} +} \ No newline at end of file diff --git a/app/secretary/login/page.tsx b/app/secretary/login/page.tsx index 3dabba8..fb9310c 100644 --- a/app/secretary/login/page.tsx +++ b/app/secretary/login/page.tsx @@ -1,11 +1,31 @@ // Caminho: app/(secretary)/login/page.tsx import { LoginForm } from "@/components/LoginForm"; +import Link from "next/link"; // Adicionado para o link de "Voltar" export default function SecretaryLoginPage() { + // NOTA: Esta página se tornou obsoleta com a criação do /login central. + // O ideal no futuro é deletar esta página e redirecionar os usuários. + return (
- -
+
+

Área da Secretária

+

Acesse o sistema de gerenciamento

+ + {/* --- ALTERAÇÃO PRINCIPAL AQUI --- */} + {/* Chamando o LoginForm unificado sem props desnecessárias */} + + {/* Adicionamos um link de "Voltar" como filho (children) */} +
+ + + Voltar à página inicial + + +
+
+
+ ); -} +} \ No newline at end of file diff --git a/components/LoginForm.tsx b/components/LoginForm.tsx index 71a6ab4..1176948 100644 --- a/components/LoginForm.tsx +++ b/components/LoginForm.tsx @@ -1,223 +1,157 @@ // Caminho: components/LoginForm.tsx -"use client"; +"use client" -import type React from "react"; -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import Link from "next/link"; -import Cookies from "js-cookie"; -import { jwtDecode } from "jwt-decode"; -import { cn } from "@/lib/utils"; +import type React from "react" +import { useState } from "react" +import { useRouter } from "next/navigation" +import Link from "next/link" +import { cn } from "@/lib/utils" + +// Nossos serviços de API centralizados +import { loginWithEmailAndPassword, api } from "@/services/api"; // Componentes Shadcn UI -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; -import { apikey } from "@/services/api.mjs"; - -// Hook customizado -import { useToast } from "@/hooks/use-toast"; +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Card, CardContent } from "@/components/ui/card" +import { useToast } from "@/hooks/use-toast" // Ícones -import { Eye, EyeOff, Mail, Lock, Loader2, UserCheck, Stethoscope, IdCard, Receipt } from "lucide-react"; +import { Eye, EyeOff, Loader2, Mail, Lock } from "lucide-react" interface LoginFormProps { - title: string; - description: string; - role: "secretary" | "doctor" | "patient" | "admin" | "manager" | "finance"; - themeColor: "blue" | "green" | "orange"; - redirectPath: string; - children?: React.ReactNode; + children?: React.ReactNode } interface FormState { - email: string; - password: string; + email: string + password: string } -// Supondo que o payload do seu token tenha esta estrutura -interface DecodedToken { - name: string; - email: string; - role: string; - exp: number; - // Adicione outros campos que seu token possa ter -} +export function LoginForm({ children }: LoginFormProps) { + const [form, setForm] = useState({ email: "", password: "" }) + const [showPassword, setShowPassword] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const router = useRouter() + const { toast } = useToast() -const themeClasses = { - blue: { - iconBg: "bg-blue-100", - iconText: "text-blue-600", - button: "bg-blue-600 hover:bg-blue-700", - link: "text-blue-600 hover:text-blue-700", - focus: "focus:border-blue-500 focus:ring-blue-500", - }, - green: { - iconBg: "bg-green-100", - iconText: "text-green-600", - button: "bg-green-600 hover:bg-green-700", - link: "text-green-600 hover:text-green-700", - focus: "focus:border-green-500 focus:ring-green-500", - }, - orange: { - iconBg: "bg-orange-100", - iconText: "text-orange-600", - button: "bg-orange-600 hover:bg-orange-700", - link: "text-orange-600 hover:text-orange-700", - focus: "focus:border-orange-500 focus:ring-orange-500", - }, -}; + // ================================================================== + // LÓGICA DE LOGIN INTELIGENTE E CENTRALIZADA + // ================================================================== + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + localStorage.removeItem("token"); + localStorage.removeItem("user_info"); -const roleIcons = { - secretary: UserCheck, - patient: Stethoscope, - doctor: Stethoscope, - admin: UserCheck, - manager: IdCard, - finance: Receipt, -}; - -export function LoginForm({ title, description, role, themeColor, redirectPath, children }: LoginFormProps) { - const [form, setForm] = useState({ email: "", password: "" }); - const [showPassword, setShowPassword] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const router = useRouter(); - const { toast } = useToast(); - - const currentTheme = themeClasses[themeColor]; - const Icon = roleIcons[role]; - - // ================================================================== - // AJUSTE PRINCIPAL NA LÓGICA DE LOGIN - // ================================================================== - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setIsLoading(true); - - const LOGIN_URL = "https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password"; - const API_KEY = apikey; - - if (!API_KEY) { - toast({ - title: "Erro de Configuração", - description: "A chave da API não foi encontrada.", - }); - setIsLoading(false); - return; + try { + const authData = await loginWithEmailAndPassword(form.email, form.password); + const user = authData.user; + if (!user || !user.id) { + throw new Error("Resposta de autenticação inválida: ID do usuário não encontrado."); } - try { - const response = await fetch(LOGIN_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - apikey: API_KEY, - }, - body: JSON.stringify({ email: form.email, password: form.password }), - }); + const rolesData = await api.get(`/rest/v1/user_roles?user_id=eq.${user.id}&select=role`); - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.error_description || "Credenciais inválidas. Tente novamente."); - } - - const accessToken = data.access_token; - const user = data.user; - - /* =================== Verificação de Role Desativada Temporariamente =================== */ - // if (user.user_metadata.role !== role) { - // toast({ title: "Acesso Negado", ... }); - // return; - // } - /* ===================================================================================== */ - - Cookies.set("access_token", accessToken, { expires: 1, secure: true }); - localStorage.setItem("user_info", JSON.stringify(user)); - - toast({ - title: "Login bem-sucedido!", - description: `Bem-vindo(a), ${user.user_metadata.full_name || "usuário"}! Redirecionando...`, - }); - - router.push(redirectPath); - } catch (error) { - toast({ - title: "Erro no Login", - description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.", - }); - } finally { - setIsLoading(false); + if (!rolesData || rolesData.length === 0) { + throw new Error("Login bem-sucedido, mas nenhum perfil de acesso foi encontrado para este usuário."); } - }; - // O JSX do return permanece exatamente o mesmo, preservando seus ajustes. - return ( - - -
- -
-
- {title} - {description} -
-
- -
- {/* Inputs e Botão */} -
- -
- - setForm({ ...form, email: e.target.value })} className={cn("pl-11 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} /> -
-
-
- -
- - setForm({ ...form, password: e.target.value })} className={cn("pl-11 pr-12 h-12 border-slate-200", currentTheme.focus)} required disabled={isLoading} /> - -
-
- -
- {/* Conteúdo Extra (children) */} -
- {children ? ( -
-
-
-
-
-
- Novo por aqui? -
-
- {children} -
- ) : ( - <> -
- - ou -
-
- - Voltar à página inicial - -
- - )} -
-
-
- ); -} + const userRole = rolesData[0].role; + const completeUserInfo = { ...user, user_metadata: { ...user.user_metadata, role: userRole } }; + localStorage.setItem('user_info', JSON.stringify(completeUserInfo)); + + let redirectPath = ""; + switch (userRole) { + case "admin": + case "manager": redirectPath = "/manager/home"; break; + case "medico": redirectPath = "/doctor/medicos"; break; + case "secretary": redirectPath = "/secretary/pacientes"; break; + case "patient": redirectPath = "/patient/dashboard"; break; + case "finance": redirectPath = "/finance/home"; break; + } + + if (!redirectPath) { + throw new Error(`O perfil de acesso '${userRole}' não é válido para login. Contate o suporte.`); + } + + toast({ + title: "Login bem-sucedido!", + description: `Bem-vindo(a)! Redirecionando...`, + }); + + router.push(redirectPath); + + } catch (error) { + localStorage.removeItem("token"); + localStorage.removeItem("user_info"); + + console.error("ERRO DETALHADO NO CATCH:", error); + + toast({ + title: "Erro no Login", + description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.", + }); + } finally { + setIsLoading(false); + } + } + + // ================================================================== + // JSX VISUALMENTE RICO E UNIFICADO + // ================================================================== + return ( + // Usamos Card e CardContent para manter a consistência, mas o estilo principal + // virá da página 'app/login/page.tsx' que envolve este componente. + + {/* Removemos o padding para dar controle à página pai */} +
+
+ +
+ + setForm({ ...form, email: e.target.value })} + className="pl-10 h-11" + required + disabled={isLoading} + autoComplete="username" // Boa prática de acessibilidade + /> +
+
+
+ +
+ + setForm({ ...form, password: e.target.value })} + className="pl-10 pr-12 h-11" + required + disabled={isLoading} + autoComplete="current-password" // Boa prática de acessibilidade + /> + +
+
+ +
+ + {/* O children permite que a página de login adicione links extras aqui */} + {children} +
+
+ ) +} \ No newline at end of file diff --git a/components/doctor-layout.tsx b/components/doctor-layout.tsx index 767db28..c9134ba 100644 --- a/components/doctor-layout.tsx +++ b/components/doctor-layout.tsx @@ -4,7 +4,8 @@ import type React from "react"; import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; -import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA +import Cookies from "js-cookie"; // Manteremos para o logout, se necessário +import { api } from '@/services/api.mjs'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -39,23 +40,20 @@ export default function DoctorLayout({ children }: PatientLayoutProps) { const router = useRouter(); const pathname = usePathname(); - // ================================================================== - // 2. BLOCO DE SEGURANÇA CORRIGIDO - // ================================================================== useEffect(() => { const userInfoString = localStorage.getItem("user_info"); - const token = Cookies.get("access_token"); + // --- ALTERAÇÃO PRINCIPAL AQUI --- + // Procurando o token no localStorage, onde ele foi realmente salvo. + const token = localStorage.getItem("token"); if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - // 3. "TRADUZIMOS" os dados da API para o formato que o layout espera setDoctorData({ id: userInfo.id || "", name: userInfo.user_metadata?.full_name || "Doutor(a)", email: userInfo.email || "", specialty: userInfo.user_metadata?.specialty || "Especialidade", - // Campos que não vêm do login, definidos como vazios para não quebrar phone: userInfo.phone || "", cpf: "", crm: "", @@ -63,35 +61,49 @@ export default function DoctorLayout({ children }: PatientLayoutProps) { permissions: {}, }); } else { - // Se faltar o token ou os dados, volta para o login - router.push("/doctor/login"); + // Se não encontrar, aí sim redireciona. + router.push("/login"); } }, [router]); + // O restante do seu código permanece exatamente o mesmo... useEffect(() => { - const handleResize = () => setWindowWidth(window.innerWidth); - handleResize(); // inicializa com a largura atual - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); -}, []); + const handleResize = () => setWindowWidth(window.innerWidth); + handleResize(); + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); -useEffect(() => { - if (isMobile) { - setSidebarCollapsed(true); - } else { - setSidebarCollapsed(false); - } -}, [isMobile]); + useEffect(() => { + if (isMobile) { + setSidebarCollapsed(true); + } else { + setSidebarCollapsed(false); + } + }, [isMobile]); const handleLogout = () => { setShowLogoutDialog(true); }; - const confirmLogout = () => { - localStorage.removeItem("doctorData"); + + // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui + } finally { + // A responsabilidade do componente é apenas limpar o estado local e redirecionar + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + setShowLogoutDialog(false); - router.push("/"); - }; + router.push("/"); // Redireciona para a home + } + }; const cancelLogout = () => { setShowLogoutDialog(false); @@ -102,30 +114,10 @@ useEffect(() => { }; const menuItems = [ - { - href: "#", - icon: Home, - label: "Dashboard", - // Botão para o dashboard do médico - }, - { - href: "/doctor/medicos/consultas", - icon: Calendar, - label: "Consultas", - // Botão para página de consultas marcadas do médico atual - }, - { - href: "#", - icon: Clock, - label: "Editor de Laudo", - // Botão para página do editor de laudo - }, - { - href: "/doctor/medicos", - icon: User, - label: "Pacientes", - // Botão para a página de visualização de todos os pacientes - }, + { href: "#", icon: Home, label: "Dashboard" }, + { href: "/doctor/medicos/consultas", icon: Calendar, label: "Consultas" }, + { href: "#", icon: Clock, label: "Editor de Laudo" }, + { href: "/doctor/medicos", icon: User, label: "Pacientes" }, ]; if (!doctorData) { @@ -133,8 +125,8 @@ useEffect(() => { } return ( + // O restante do seu código JSX permanece exatamente o mesmo
- {/* Sidebar para desktop */}
@@ -170,7 +162,6 @@ useEffect(() => {
- {/* Se a sidebar estiver recolhida, o avatar e o texto do usuário também devem ser condensados ou ocultados */} {!sidebarCollapsed && ( <> @@ -189,7 +180,7 @@ useEffect(() => { )} {sidebarCollapsed && ( - {/* Centraliza o avatar quando recolhido */} + {doctorData.name @@ -201,7 +192,6 @@ useEffect(() => { )}
- {/* Novo botão de sair, usando a mesma estrutura dos itens de menu */}
{ const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href)); return ( - {/* Fechar menu ao clicar */} +
{item.label} @@ -259,17 +249,14 @@ useEffect(() => {

{doctorData.specialty}

-
- - {/* Main Content */} -
- {/* Header */} +
@@ -288,11 +275,9 @@ useEffect(() => {
- {/* Page Content */}
{children}
- {/* Logout confirmation dialog */} diff --git a/components/finance-layout.tsx b/components/finance-layout.tsx index abcc7eb..d8b4bd2 100644 --- a/components/finance-layout.tsx +++ b/components/finance-layout.tsx @@ -1,3 +1,4 @@ +// Caminho: [seu-caminho]/FinancierLayout.tsx "use client"; import Cookies from "js-cookie"; @@ -5,32 +6,14 @@ import type React from "react"; import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; +import { api } from '@/services/api.mjs'; + import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Search, - Bell, - Calendar, - Clock, - User, - LogOut, - Menu, - X, - Home, - FileText, - ChevronLeft, - ChevronRight, -} from "lucide-react"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Search, Bell, Calendar, Clock, User, LogOut, Menu, X, Home, FileText, ChevronLeft, ChevronRight } from "lucide-react"; interface FinancierData { id: string; @@ -47,37 +30,45 @@ interface PatientLayoutProps { } export default function FinancierLayout({ children }: PatientLayoutProps) { - const [financierData, setFinancierData] = useState( - null - ); + const [financierData, setFinancierData] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [showLogoutDialog, setShowLogoutDialog] = useState(false); const router = useRouter(); const pathname = usePathname(); useEffect(() => { - const data = localStorage.getItem("financierData"); - if (data) { - setFinancierData(JSON.parse(data)); + const userInfoString = localStorage.getItem("user_info"); + // --- ALTERAÇÃO 1: Buscando o token no localStorage --- + const token = localStorage.getItem("token"); + + if (userInfoString && token) { + const userInfo = JSON.parse(userInfoString); + + setFinancierData({ + id: userInfo.id || "", + name: userInfo.user_metadata?.full_name || "Financeiro", + email: userInfo.email || "", + department: userInfo.user_metadata?.department || "Departamento Financeiro", + phone: userInfo.phone || "", + cpf: "", + permissions: {}, + }); } else { - router.push("/finance/login"); + // --- ALTERAÇÃO 2: Redirecionando para o login central --- + router.push("/login"); } }, [router]); - // 🔥 Responsividade automática da sidebar useEffect(() => { const handleResize = () => { - // Ajuste o breakpoint conforme necessário. 1024px (lg) ou 768px (md) são comuns. if (window.innerWidth < 1024) { setSidebarCollapsed(true); } else { setSidebarCollapsed(false); } }; - - handleResize(); // executa na primeira carga + handleResize(); window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); }, []); @@ -85,10 +76,22 @@ export default function FinancierLayout({ children }: PatientLayoutProps) { setShowLogoutDialog(true); }; - const confirmLogout = () => { - localStorage.removeItem("financierData"); - setShowLogoutDialog(false); - router.push("/"); + // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui + } finally { + // A responsabilidade do componente é apenas limpar o estado local e redirecionar + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a home + } }; const cancelLogout = () => { @@ -96,35 +99,19 @@ export default function FinancierLayout({ children }: PatientLayoutProps) { }; const menuItems = [ - { - href: "#", - icon: Home, - label: "Dashboard", - }, - { - href: "#", - icon: Calendar, - label: "Relatórios financeiros", - }, - { - href: "#", - icon: User, - label: "Finanças Gerais", - }, - { - href: "#", - icon: Calendar, - label: "Configurações", - }, + { href: "#", icon: Home, label: "Dashboard" }, + { href: "#", icon: Calendar, label: "Relatórios financeiros" }, + { href: "#", icon: User, label: "Finanças Gerais" }, + { href: "#", icon: Calendar, label: "Configurações" }, ]; if (!financierData) { - return
Carregando...
; + return
Carregando...
; } return ( + // O restante do seu código JSX permanece inalterado
- {/* Sidebar */}
- {/* Footer user info */}
@@ -206,34 +192,29 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
)}
- {/* Botão Sair - ajustado para responsividade */}
- {/* Main Content */}
- {/* Header */}
@@ -257,11 +238,9 @@ export default function FinancierLayout({ children }: PatientLayoutProps) {
- {/* Page Content */}
{children}
- {/* Logout confirmation dialog */} diff --git a/components/manager-layout.tsx b/components/manager-layout.tsx index 9d2600b..3af99ef 100644 --- a/components/manager-layout.tsx +++ b/components/manager-layout.tsx @@ -1,33 +1,19 @@ +// Caminho: [seu-caminho]/ManagerLayout.tsx "use client"; import type React from "react"; import { useState, useEffect } from "react"; import { useRouter, usePathname } from "next/navigation"; import Link from "next/link"; -import Cookies from "js-cookie"; // <-- 1. IMPORTAÇÃO ADICIONADA +import Cookies from "js-cookie"; // Mantido apenas para a limpeza de segurança no logout +import { api } from '@/services/api.mjs'; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Search, - Bell, - Calendar, - User, - LogOut, - ChevronLeft, - ChevronRight, - Home, -} from "lucide-react"; +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Search, Bell, Calendar, User, LogOut, ChevronLeft, ChevronRight, Home } from "lucide-react"; interface ManagerData { id: string; @@ -39,7 +25,7 @@ interface ManagerData { permissions: object; } -interface ManagerLayoutProps { // Corrigi o nome da prop aqui +interface ManagerLayoutProps { children: React.ReactNode; } @@ -50,89 +36,88 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { const router = useRouter(); const pathname = usePathname(); - // ================================================================== - // 2. BLOCO DE SEGURANÇA CORRIGIDO - // ================================================================== useEffect(() => { const userInfoString = localStorage.getItem("user_info"); - const token = Cookies.get("access_token"); + // --- ALTERAÇÃO 1: Buscando o token no localStorage --- + const token = localStorage.getItem("token"); if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - // 3. "TRADUZIMOS" os dados da API para o formato que o layout espera setManagerData({ id: userInfo.id || "", name: userInfo.user_metadata?.full_name || "Gestor(a)", email: userInfo.email || "", department: userInfo.user_metadata?.role || "Gestão", - // Campos que não vêm do login, definidos como vazios para não quebrar phone: userInfo.phone || "", cpf: "", permissions: {}, }); } else { - // Se faltar o token ou os dados, volta para o login - router.push("/manager/login"); + // O redirecionamento para /login já estava correto. Ótimo! + router.push("/login"); } }, [router]); - - // 🔥 Responsividade automática da sidebar useEffect(() => { const handleResize = () => { if (window.innerWidth < 1024) { - setSidebarCollapsed(true); // colapsa em telas pequenas (lg breakpoint ~ 1024px) + setSidebarCollapsed(true); } else { - setSidebarCollapsed(false); // expande em desktop + setSidebarCollapsed(false); } }; - - handleResize(); // roda na primeira carga + handleResize(); window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); }, []); const handleLogout = () => setShowLogoutDialog(true); - const confirmLogout = () => { - localStorage.removeItem("managerData"); - setShowLogoutDialog(false); - router.push("/"); + // --- ALTERAÇÃO 2: A função de logout agora é MUITO mais simples --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + // O erro já é logado dentro da função api.logout, não precisamos fazer nada aqui + } finally { + // A responsabilidade do componente é apenas limpar o estado local e redirecionar + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a home + } }; const cancelLogout = () => setShowLogoutDialog(false); const menuItems = [ - { href: "#", icon: Home, label: "Dashboard" }, - { href: "#", icon: Calendar, label: "Relatórios gerenciais" }, - { href: "#", icon: User, label: "Gestão de Usuários" }, - { href: "#", icon: User, label: "Gestão de Médicos" }, - { href: "#", icon: Calendar, label: "Configurações" }, + { href: "#dashboard", icon: Home, label: "Dashboard" }, + { href: "#reports", icon: Calendar, label: "Relatórios gerenciais" }, + { href: "#users", icon: User, label: "Gestão de Usuários" }, + { href: "#doctors", icon: User, label: "Gestão de Médicos" }, + { href: "#settings", icon: Calendar, label: "Configurações" }, ]; if (!managerData) { - return
Carregando...
; + return
Carregando...
; } return (
- {/* Sidebar */}
- {/* Logo + collapse button */}
{!sidebarCollapsed && (
- - MidConnecta - + MidConnecta
)}
- {/* Menu Items */} - {/* Perfil no rodapé */}
- - {managerData.name - .split(" ") - .map((n) => n[0]) - .join("")} - + {managerData.name.split(" ").map((n) => n[0]).join("")} {!sidebarCollapsed && (
-

- {managerData.name} -

-

- {managerData.department} -

+

{managerData.name}

+

{managerData.department}

)}
- {/* Botão Sair - ajustado para responsividade */}
- {/* Conteúdo principal */} -
- {/* Header */} +
- {/* Search */}
- +
- - {/* Notifications */}
- - {/* Page Content */}
{children}
- {/* Logout confirmation dialog */} Confirmar Saída - - Deseja realmente sair do sistema? Você precisará fazer login - novamente para acessar sua conta. - + Deseja realmente sair do sistema? Você precisará fazer login novamente para acessar sua conta. - - + + diff --git a/components/patient-layout.tsx b/components/patient-layout.tsx index 9ff3aa1..775dc5f 100644 --- a/components/patient-layout.tsx +++ b/components/patient-layout.tsx @@ -1,36 +1,18 @@ "use client" - import Cookies from "js-cookie"; import type React from "react" import { useState, useEffect } from "react" import Link from "next/link" import { useRouter, usePathname } from "next/navigation" +import { api } from "@/services/api.mjs"; // Importando nosso cliente de API + import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { - Search, - Bell, - User, - LogOut, - FileText, - Clock, - Calendar, - Home, - ChevronLeft, - ChevronRight, -} from "lucide-react" - -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" +import { Search, Bell, User, LogOut, FileText, Clock, Calendar, Home, ChevronLeft, ChevronRight } from "lucide-react" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" interface PatientData { name: string @@ -41,65 +23,72 @@ interface PatientData { address: string } -interface HospitalLayoutProps { +interface PatientLayoutProps { children: React.ReactNode } -export default function HospitalLayout({ children }: HospitalLayoutProps) { +// --- ALTERAÇÃO 1: Renomeando o componente para maior clareza --- +export default function PatientLayout({ children }: PatientLayoutProps) { const [patientData, setPatientData] = useState(null) const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [showLogoutDialog, setShowLogoutDialog] = useState(false) const router = useRouter() const pathname = usePathname() - // 🔹 Ajuste automático no resize useEffect(() => { const handleResize = () => { if (window.innerWidth < 1024) { - setSidebarCollapsed(true) // colapsa no mobile + setSidebarCollapsed(true) } else { - setSidebarCollapsed(false) // expande no desktop + setSidebarCollapsed(false) } } - handleResize() window.addEventListener("resize", handleResize) return () => window.removeEventListener("resize", handleResize) }, []) useEffect(() => { - // 1. Procuramos pela chave correta: 'user_info' const userInfoString = localStorage.getItem("user_info"); - // 2. Para mais segurança, verificamos também se o token de acesso existe no cookie - const token = Cookies.get("access_token"); + // --- ALTERAÇÃO 2: Buscando o token no localStorage --- + const token = localStorage.getItem("token"); if (userInfoString && token) { const userInfo = JSON.parse(userInfoString); - - // 3. Adaptamos os dados para a estrutura que seu layout espera (PatientData) - // Usamos os dados do objeto 'user' que a API do Supabase nos deu + setPatientData({ name: userInfo.user_metadata?.full_name || "Paciente", email: userInfo.email || "", - // Os campos abaixo não vêm do login, então os deixamos vazios por enquanto phone: userInfo.phone || "", cpf: "", birthDate: "", address: "", }); } else { - // Se as informações do usuário ou o token não forem encontrados, mandamos para o login. - router.push("/patient/login"); + // --- ALTERAÇÃO 3: Redirecionando para o login central --- + router.push("/login"); } }, [router]); const handleLogout = () => setShowLogoutDialog(true) - const confirmLogout = () => { - localStorage.removeItem("patientData") - setShowLogoutDialog(false) - router.push("/") - } + // --- ALTERAÇÃO 4: Função de logout completa e padronizada --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + console.error("Erro ao tentar fazer logout no servidor:", error); + } finally { + // Limpeza completa e consistente do estado local + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a página inicial + } + }; const cancelLogout = () => setShowLogoutDialog(false) @@ -112,7 +101,7 @@ export default function HospitalLayout({ children }: HospitalLayoutProps) { ] if (!patientData) { - return
Carregando...
+ return
Carregando...
; } return ( diff --git a/components/secretary-layout.tsx b/components/secretary-layout.tsx index a52b7bf..1ccb2b3 100644 --- a/components/secretary-layout.tsx +++ b/components/secretary-layout.tsx @@ -1,3 +1,4 @@ +// Caminho: app/(secretary)/layout.tsx (ou o caminho do seu arquivo) "use client" import type React from "react" @@ -5,30 +6,14 @@ import { useState, useEffect } from "react" import { useRouter, usePathname } from "next/navigation" import Link from "next/link" import Cookies from "js-cookie"; +import { api } from '@/services/api.mjs'; // Importando nosso cliente de API central + import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog" - -import { - Search, - Bell, - Calendar, - Clock, - User, - LogOut, - Home, - ChevronLeft, - ChevronRight, -} from "lucide-react" +import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Search, Bell, Calendar, Clock, User, LogOut, Home, ChevronLeft, ChevronRight } from "lucide-react" interface SecretaryData { id: string @@ -46,12 +31,36 @@ interface SecretaryLayoutProps { } export default function SecretaryLayout({ children }: SecretaryLayoutProps) { + const [secretaryData, setSecretaryData] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [showLogoutDialog, setShowLogoutDialog] = useState(false) const router = useRouter() const pathname = usePathname() - // 🔹 Colapsar no mobile e expandir no desktop automaticamente + useEffect(() => { + const userInfoString = localStorage.getItem("user_info"); + // --- ALTERAÇÃO 1: Buscando o token no localStorage --- + const token = localStorage.getItem("token"); + + if (userInfoString && token) { + const userInfo = JSON.parse(userInfoString); + + setSecretaryData({ + id: userInfo.id || "", + name: userInfo.user_metadata?.full_name || "Secretária", + email: userInfo.email || "", + department: userInfo.user_metadata?.department || "Atendimento", + phone: userInfo.phone || "", + cpf: "", + employeeId: "", + permissions: {}, + }); + } else { + // --- ALTERAÇÃO 2: Redirecionando para o login central --- + router.push("/login"); + } + }, [router]); + useEffect(() => { const handleResize = () => { if (window.innerWidth < 1024) { @@ -66,10 +75,25 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) { }, []) const handleLogout = () => setShowLogoutDialog(true) - const confirmLogout = () => { - setShowLogoutDialog(false) - router.push("/") - } + + // --- ALTERAÇÃO 3: Função de logout completa e padronizada --- + const confirmLogout = async () => { + try { + // Chama a função centralizada para fazer o logout no servidor + await api.logout(); + } catch (error) { + console.error("Erro ao tentar fazer logout no servidor:", error); + } finally { + // Limpeza completa e consistente do estado local + localStorage.removeItem("user_info"); + localStorage.removeItem("token"); + Cookies.remove("access_token"); // Limpeza de segurança + + setShowLogoutDialog(false); + router.push("/"); // Redireciona para a página inicial + } + }; + const cancelLogout = () => setShowLogoutDialog(false) const menuItems = [ @@ -79,17 +103,11 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) { { href: "/secretary/pacientes", icon: User, label: "Pacientes" }, ] - const secretaryData: SecretaryData = { - id: "1", - name: "Secretária Exemplo", - email: "secretaria@hospital.com", - phone: "999999999", - cpf: "000.000.000-00", - employeeId: "12345", - department: "Atendimento", - permissions: {}, + if (!secretaryData) { + return
Carregando...
; } + return (
{/* Sidebar */} @@ -165,23 +183,20 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
)}
- {/* Botão Sair - ajustado para responsividade */}
@@ -191,7 +206,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) { className={`flex-1 flex flex-col transition-all duration-300 ${sidebarCollapsed ? "ml-16" : "ml-64" }`} > - {/* Header */}
@@ -205,13 +219,6 @@ export default function SecretaryLayout({ children }: SecretaryLayoutProps) {
- {/* Este botão no header parece ter sido uma cópia do botão "Sair" da sidebar. - Removi a lógica de sidebarCollapsed aqui, pois o header é independente. - Se a intenção era ter um botão de logout no header, ele não deve ser afetado pela sidebar. - Ajustei para ser um botão de sino de notificação, como nos exemplos anteriores, - já que você tem o ícone Bell importado e uma badge para notificação. - Se você quer um botão de LogOut aqui, por favor, me avise! - */}
- {/* Page Content */}
{children}
diff --git a/services/api.mjs b/services/api.mjs index 6c053b9..8fe59e9 100644 --- a/services/api.mjs +++ b/services/api.mjs @@ -1,47 +1,59 @@ +// Caminho: [seu-caminho]/services/api.mjs + const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co"; const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; -export const apikey = API_KEY; -var tempToken; -export async function login() { - const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password", { +export async function loginWithEmailAndPassword(email, password) { + const response = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, { method: "POST", headers: { "Content-Type": "application/json", - Prefer: "return=representation", - apikey: API_KEY, // valor fixo + "apikey": API_KEY, }, - body: JSON.stringify({ email: "riseup@popcode.com.br", password: "riseup" }), + body: JSON.stringify({ email, password }), }); const data = await response.json(); - if (typeof window !== 'undefined') { + if (!response.ok) { + throw new Error(data.error_description || "Credenciais inválidas."); + } + + if (data.access_token && typeof window !== 'undefined') { + // Padronizando para salvar o token no localStorage localStorage.setItem("token", data.access_token); } return data; } -let loginPromise = login(); +// --- NOVA FUNÇÃO DE LOGOUT CENTRALIZADA --- +async function logout() { + const token = localStorage.getItem("token"); + if (!token) return; // Se não há token, não há o que fazer + + try { + await fetch(`${BASE_URL}/auth/v1/logout`, { + method: "POST", + headers: { + "apikey": API_KEY, + "Authorization": `Bearer ${token}`, + }, + }); + } catch (error) { + // Mesmo que a chamada falhe, o logout no cliente deve continuar. + // O token pode já ter expirado no servidor, por exemplo. + console.error("Falha ao invalidar token no servidor (isso pode ser normal se o token já expirou):", error); + } +} async function request(endpoint, options = {}) { - if (loginPromise) { - try { - await loginPromise; - } catch (error) { - console.error("Falha na autenticação inicial:", error); - } - - loginPromise = null; - } - const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null; const headers = { "Content-Type": "application/json", - apikey: API_KEY, - ...(token ? { Authorization: `Bearer ${token}` } : {}), + "apikey": API_KEY, + ...(token ? { "Authorization": `Bearer ${token}` } : {}), ...options.headers, }; @@ -52,35 +64,29 @@ async function request(endpoint, options = {}) { }); if (!response.ok) { - let errorBody = `Status: ${response.status}`; + let errorBody; try { - const contentType = response.headers.get("content-type"); - if (contentType && contentType.includes("application/json")) { - const jsonError = await response.json(); - - errorBody = jsonError.message || JSON.stringify(jsonError); - } else { - errorBody = await response.text(); - } + errorBody = await response.json(); } catch (e) { - errorBody = `Status: ${response.status} - Falha ao ler corpo do erro.`; + errorBody = await response.text(); } - - throw new Error(`Erro HTTP: ${response.status} - Detalhes: ${errorBody}`); - } - const contentType = response.headers.get("content-type"); - if (response.status === 204 || (contentType && !contentType.includes("application/json")) || !contentType) { - return {}; + throw new Error(`Erro HTTP: ${response.status} - ${JSON.stringify(errorBody)}`); } + + if (response.status === 204) return {}; return await response.json(); + } catch (error) { console.error("Erro na requisição:", error); throw error; } } + +// Adicionamos a função de logout ao nosso objeto de API exportado export const api = { get: (endpoint, options) => request(endpoint, { method: "GET", ...options }), - post: (endpoint, data) => request(endpoint, { method: "POST", body: JSON.stringify(data) }), - patch: (endpoint, data) => request(endpoint, { method: "PATCH", body: JSON.stringify(data) }), - delete: (endpoint) => request(endpoint, { method: "DELETE" }), -}; + post: (endpoint, data, options) => request(endpoint, { method: "POST", body: JSON.stringify(data), ...options }), + patch: (endpoint, data, options) => request(endpoint, { method: "PATCH", body: JSON.stringify(data), ...options }), + delete: (endpoint, options) => request(endpoint, { method: "DELETE", ...options }), + logout: logout, // <-- EXPORTANDO A NOVA FUNÇÃO +}; \ No newline at end of file From eb58c014d94cdbe832f7996aa7d578338a79c288 Mon Sep 17 00:00:00 2001 From: Gabriel Lira Figueira Date: Wed, 15 Oct 2025 23:51:00 -0300 Subject: [PATCH 6/9] teste --- .vscode/settings.json | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file From 6d1f889397428931c8d8fdf2cc4621b58993bcec Mon Sep 17 00:00:00 2001 From: Lucas Rodrigues Date: Thu, 16 Oct 2025 00:16:25 -0300 Subject: [PATCH 7/9] ajustando --- app/manager/usuario/novo/page.tsx | 65 +++++++++++++++---------------- services/api.mjs | 36 ++++++++++++++--- services/usersApi.mjs | 4 +- 3 files changed, 64 insertions(+), 41 deletions(-) diff --git a/app/manager/usuario/novo/page.tsx b/app/manager/usuario/novo/page.tsx index ee73c95..06227c9 100644 --- a/app/manager/usuario/novo/page.tsx +++ b/app/manager/usuario/novo/page.tsx @@ -18,14 +18,13 @@ import ManagerLayout from "@/components/manager-layout"; import { usersService } from "services/usersApi.mjs"; import { login } from "services/api.mjs"; -// Adicionada a propriedade 'senha' e 'confirmarSenha' interface UserFormData { email: string; nomeCompleto: string; telefone: string; papel: string; senha: string; - confirmarSenha: string; // Novo campo para confirmação + confirmarSenha: string; } const defaultFormData: UserFormData = { @@ -37,7 +36,6 @@ const defaultFormData: UserFormData = { confirmarSenha: "", }; -// Funções de formatação de telefone const cleanNumber = (value: string): string => value.replace(/\D/g, ""); const formatPhone = (value: string): string => { const cleaned = cleanNumber(value).substring(0, 11); @@ -63,13 +61,17 @@ export default function NovoUsuarioPage() { e.preventDefault(); setError(null); - // Validação de campos obrigatórios - if (!formData.email || !formData.nomeCompleto || !formData.papel || !formData.senha || !formData.confirmarSenha) { + if ( + !formData.email || + !formData.nomeCompleto || + !formData.papel || + !formData.senha || + !formData.confirmarSenha + ) { setError("Por favor, preencha todos os campos obrigatórios."); return; } - // Validação de senhas if (formData.senha !== formData.confirmarSenha) { setError("A Senha e a Confirmação de Senha não coincidem."); return; @@ -85,19 +87,20 @@ export default function NovoUsuarioPage() { email: formData.email.trim().toLowerCase(), phone: formData.telefone || null, role: formData.papel, - password: formData.senha, // Senha adicionada + password: formData.senha, }; console.log("📤 Enviando payload:", payload); + await usersService.create_user(payload); router.push("/manager/usuario"); } catch (e: any) { console.error("Erro ao criar usuário:", e); - const msg = - e.message || - "Não foi possível criar o usuário. Verifique os dados e tente novamente."; - setError(msg); + setError( + e?.message || + "Não foi possível criar o usuário. Verifique os dados e tente novamente." + ); } finally { setIsSaving(false); } @@ -105,14 +108,13 @@ export default function NovoUsuarioPage() { return ( - {/* Container principal: w-full e centralizado. max-w-screen-lg para evitar expansão excessiva */}
- - {/* Cabeçalho */}
-

Novo Usuário

+

+ Novo Usuário +

Preencha os dados para cadastrar um novo usuário no sistema.

@@ -122,7 +124,6 @@ export default function NovoUsuarioPage() {
- {/* Formulário */}

Erro no Cadastro:

-

{error}

+

{error}

)} - {/* Campos de Entrada - Usando Grid de 2 colunas para organização */}
- - {/* Nome Completo - Largura total */}
- {/* E-mail (Coluna 1) */}
- {/* Papel (Função) (Coluna 2) */}
handleInputChange("senha", e.target.value)} placeholder="Mínimo 8 caracteres" - minLength={8} + minLength={8} required />
- {/* Confirmar Senha (Coluna 2) */}
handleInputChange("confirmarSenha", e.target.value)} + onChange={(e) => + handleInputChange("confirmarSenha", e.target.value) + } placeholder="Repita a senha" required /> - {formData.senha && formData.confirmarSenha && formData.senha !== formData.confirmarSenha && ( -

As senhas não coincidem.

- )} + {formData.senha && + formData.confirmarSenha && + formData.senha !== formData.confirmarSenha && ( +

+ As senhas não coincidem. +

+ )}
- - {/* Telefone (Opcional, mas mantido no grid) */} +
-
- {/* Botões de Ação */}
); -} \ No newline at end of file +} diff --git a/services/api.mjs b/services/api.mjs index 17f8a0c..6894d9f 100644 --- a/services/api.mjs +++ b/services/api.mjs @@ -5,8 +5,8 @@ const API_KEY = export const apikey = API_KEY; let loginPromise = null; -// 🔹 Autenticação export async function login() { + console.log("🔐 Iniciando login..."); const res = await fetch(`${BASE_URL}/auth/v1/token?grant_type=password`, { method: "POST", headers: { @@ -20,10 +20,19 @@ export async function login() { }), }); + if (!res.ok) { + const msg = await res.text(); + console.error("❌ Erro no login:", res.status, msg); + throw new Error(`Erro ao autenticar: ${res.status} - ${msg}`); + } + const data = await res.json(); - if (typeof window !== "undefined") { + console.log("✅ Login bem-sucedido:", data); + + if (typeof window !== "undefined" && data.access_token) { localStorage.setItem("token", data.access_token); } + return data; } @@ -33,28 +42,42 @@ async function request(endpoint, options = {}) { try { await loginPromise; } catch (error) { - console.error("Falha ao autenticar:", error); + console.error("⚠️ Falha ao autenticar:", error); } finally { loginPromise = null; } - const token = + let token = typeof window !== "undefined" ? localStorage.getItem("token") : null; + if (!token) { + console.warn("⚠️ Token não encontrado, refazendo login..."); + const data = await login(); + token = data.access_token; + } + const headers = { "Content-Type": "application/json", apikey: API_KEY, - ...(token ? { Authorization: `Bearer ${token}` } : {}), + Authorization: `Bearer ${token}`, ...options.headers, }; - const response = await fetch(`${BASE_URL}${endpoint}`, { + const fullUrl = + endpoint.startsWith("/rest/v1") || endpoint.startsWith("/functions/") + ? `${BASE_URL}${endpoint}` + : `${BASE_URL}/rest/v1${endpoint}`; + + console.log("🌐 Requisição para:", fullUrl, "com headers:", headers); + + const response = await fetch(fullUrl, { ...options, headers, }); if (!response.ok) { const msg = await response.text(); + console.error("❌ Erro HTTP:", response.status, msg); throw new Error(`Erro HTTP: ${response.status} - Detalhes: ${msg}`); } @@ -71,3 +94,4 @@ export const api = { request(endpoint, { method: "PATCH", body: JSON.stringify(data) }), delete: (endpoint) => request(endpoint, { method: "DELETE" }), }; + diff --git a/services/usersApi.mjs b/services/usersApi.mjs index eb7d91e..263ada8 100644 --- a/services/usersApi.mjs +++ b/services/usersApi.mjs @@ -2,14 +2,16 @@ import { api } from "./api.mjs"; export const usersService = { async list_roles() { + // continua usando /rest/v1 normalmente return await api.get(`/rest/v1/user_roles?select=id,user_id,role,created_at`); }, async create_user(data) { + // continua usando a Edge Function corretamente return await api.post(`/functions/v1/user-create`, data); }, - // 🚀 Busca dados completos do usuário direto do banco (sem função bloqueada) + // 🚀 Busca dados completos do usuário direto do banco async full_data(user_id) { if (!user_id) throw new Error("user_id é obrigatório"); From aef7c0997cb766a12c9f3862a0cde085d7b059b4 Mon Sep 17 00:00:00 2001 From: GagoDuBroca Date: Thu, 16 Oct 2025 08:14:54 -0300 Subject: [PATCH 8/9] Pequeno ajustes --- app/doctor/dashboard/page.tsx | 2 +- app/doctor/disponibilidade/excecoes/page.tsx | 1 - app/doctor/disponibilidade/page.tsx | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index c8a6c2c..bf91156 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -273,7 +273,7 @@ export default function PatientDashboard() {

{date}

- {startTime} - {endTime} + {startTime} - {endTime}
-

diff --git a/app/doctor/disponibilidade/excecoes/page.tsx b/app/doctor/disponibilidade/excecoes/page.tsx index 6eafdde..115ff54 100644 --- a/app/doctor/disponibilidade/excecoes/page.tsx +++ b/app/doctor/disponibilidade/excecoes/page.tsx @@ -161,7 +161,6 @@ export default function ExceptionPage() {

Dados

-

{selectedCalendarDate?.toLocaleDateString("pt-BR", { weekday: "long" })}

diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx index 4a2beb0..127b93f 100644 --- a/app/doctor/disponibilidade/page.tsx +++ b/app/doctor/disponibilidade/page.tsx @@ -145,7 +145,7 @@ export default function AvailabilityPage() {
From 6e215d5ae23fe2247189e6eeec5487e5232cbc78 Mon Sep 17 00:00:00 2001 From: StsDanilo Date: Thu, 16 Oct 2025 09:04:03 -0300 Subject: [PATCH 9/9] pequeno ajuste para api --- services/availabilityApi.mjs | 11 ++++++----- services/exceptionApi.mjs | 1 + services/patientsApi.mjs | 10 +++++----- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/services/availabilityApi.mjs b/services/availabilityApi.mjs index 71be51e..40b7fcf 100644 --- a/services/availabilityApi.mjs +++ b/services/availabilityApi.mjs @@ -1,8 +1,9 @@ import { api } from "./api.mjs"; export const AvailabilityService = { - list: () => api.get("/rest/v1/doctor_availability"), - create: (data) => api.post("/rest/v1/doctor_availability", data), - update: (id, data) => api.patch(`/rest/v1/doctor_availability?id=eq.${id}`, data), - delete: (id) => api.delete(`/rest/v1/doctor_availability/{id}?id=eq.${id}`), -}; \ No newline at end of file + list: () => api.get("/rest/v1/doctor_availability"), + listById: (id) => api.get(`/rest/v1/doctor_availability?doctor_id=eq.${id}`), + create: (data) => api.post("/rest/v1/doctor_availability", data), + update: (id, data) => api.patch(`/rest/v1/doctor_availability?id=eq.${id}`, data), + delete: (id) => api.delete(`/rest/v1/doctor_availability?id=eq.${id}`), +}; diff --git a/services/exceptionApi.mjs b/services/exceptionApi.mjs index c0b3c0e..62e893e 100644 --- a/services/exceptionApi.mjs +++ b/services/exceptionApi.mjs @@ -2,6 +2,7 @@ import { api } from "./api.mjs"; export const exceptionsService = { list: () => api.get("/rest/v1/doctor_exceptions"), + listById: () => api.get(`/rest/v1/doctor_exceptions?id=eq.${id}`), create: (data) => api.post("/rest/v1/doctor_exceptions", data), delete: (id) => api.delete(`/rest/v1/doctor_exceptions?id=eq.${id}`), }; diff --git a/services/patientsApi.mjs b/services/patientsApi.mjs index 2a5ba54..5eb1e7c 100644 --- a/services/patientsApi.mjs +++ b/services/patientsApi.mjs @@ -1,9 +1,9 @@ import { api } from "./api.mjs"; export const patientsService = { - list: () => api.get("/rest/v1/patients"), - getById: (id) => api.get(`/rest/v1/patients?id=eq.${id}`), - create: (data) => api.post("/rest/v1/patients", data), - update: (id, data) => api.patch(`/rest/v1/patients?id=eq.${id}`, data), - delete: (id) => api.delete(`/rest/v1/patients?id=eq.${id}`), + list: () => api.get("/rest/v1/patients"), + getById: (id) => api.get(`/rest/v1/patients?id=eq.${id}`), + create: (data) => api.post("/rest/v1/patients", data), + update: (id, data) => api.patch(`/rest/v1/patients?id=eq.${id}`, data), + delete: (id) => api.delete(`/rest/v1/patients?id=eq.${id}`), };