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 diff --git a/app/doctor/dashboard/page.tsx b/app/doctor/dashboard/page.tsx index d9615f2..bf91156 100644 --- a/app/doctor/dashboard/page.tsx +++ b/app/doctor/dashboard/page.tsx @@ -1,10 +1,139 @@ -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, 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; + 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 = "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", + monday: "Segunda", + tuesday: "Terça", + wednesday: "Quarta", + thursday: "Quinta", + friday: "Sexta", + saturday: "Sábado", + }; + + 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}`); + } + }; + 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) => { + 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 +214,102 @@ 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

+ )} +
+
+
+ ); + })} +
+
+
+
+ + + 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..115ff54 --- /dev/null +++ b/app/doctor/disponibilidade/excecoes/page.tsx @@ -0,0 +1,218 @@ +"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

+
+
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+
+ +
+ + + + +
+
+ )} +
+
+
+
+ ); +} diff --git a/app/doctor/disponibilidade/page.tsx b/app/doctor/disponibilidade/page.tsx new file mode 100644 index 0000000..127b93f --- /dev/null +++ b/app/doctor/disponibilidade/page.tsx @@ -0,0 +1,186 @@ +"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"; +import { toast } from "@/hooks/use-toast"; +import { useRouter } from "next/navigation"; + +export default function AvailabilityPage() { + const [error, setError] = useState(null); + 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(() => { + 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: 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, + 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); + } + }; + + return ( + +
+
+
+

Definir Disponibilidade

+

Defina sua disponibilidade para consultas

+
+
+ +
+
+

Dados

+ +
+
+
+ +
+ + + + + + + +
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + + + + + + +
+
+
+
+ ); +} 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/dashboard/page.tsx b/app/manager/dashboard/page.tsx index 6e1dd10..df56541 100644 --- a/app/manager/dashboard/page.tsx +++ b/app/manager/dashboard/page.tsx @@ -1,41 +1,105 @@ -import ManagerLayout from "@/components/manager-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 ManagerLayout from "@/components/manager-layout"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Calendar, Clock, Plus, User } from "lucide-react"; +import Link from "next/link"; +import React, { useState, useEffect } from "react"; +import { usersService } from "services/usersApi.mjs"; +import { doctorsService } from "services/doctorsApi.mjs"; export default function ManagerDashboard() { + // 🔹 Estados para usuários + const [firstUser, setFirstUser] = useState(null); + const [loadingUser, setLoadingUser] = useState(true); + + // 🔹 Estados para médicos + const [doctors, setDoctors] = useState([]); + const [loadingDoctors, setLoadingDoctors] = useState(true); + + // 🔹 Buscar primeiro usuário + useEffect(() => { + async function fetchFirstUser() { + try { + const data = await usersService.list_roles(); + if (Array.isArray(data) && data.length > 0) { + setFirstUser(data[0]); + } + } catch (error) { + console.error("Erro ao carregar usuário:", error); + } finally { + setLoadingUser(false); + } + } + + fetchFirstUser(); + }, []); + + // 🔹 Buscar 3 primeiros médicos + useEffect(() => { + async function fetchDoctors() { + try { + const data = await doctorsService.list(); // ajuste se seu service tiver outro método + if (Array.isArray(data)) { + setDoctors(data.slice(0, 3)); // pega os 3 primeiros + } + } catch (error) { + console.error("Erro ao carregar médicos:", error); + } finally { + setLoadingDoctors(false); + } + } + + fetchDoctors(); + }, []); + return (
+ {/* Cabeçalho */}

Dashboard

Bem-vindo ao seu portal de consultas médicas

+ {/* Cards principais */}
+ {/* Card 1 */} Relatórios gerenciais -
3
-

2 não lidos, 1 lido

+
0
+

Relatórios disponíveis

+ {/* Card 2 — Gestão de usuários */} Gestão de usuários -
João Marques
-

fez login a 13min

+ {loadingUser ? ( +
Carregando usuário...
+ ) : firstUser ? ( + <> +
{firstUser.full_name || "Sem nome"}
+

+ {firstUser.email || "Sem e-mail cadastrado"} +

+ + ) : ( +
Nenhum usuário encontrado
+ )}
+ {/* Card 3 — Perfil */} Perfil @@ -48,66 +112,79 @@ export default function ManagerDashboard() {
+ {/* Cards secundários */}
+ {/* Card — Ações rápidas */} Ações Rápidas Acesse rapidamente as principais funcionalidades - + - - - - + + + + + + + + {/* 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/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/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/app/page.tsx b/app/page.tsx index c163489..c29ad38 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -17,7 +17,7 @@ export default function InicialPage() {

MediConnect

{} - + - +
+ 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/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

-
-
-
+ )}
- ) + ); } 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 77037a7..fe94bb2 100644 --- a/components/LoginForm.tsx +++ b/components/LoginForm.tsx @@ -1,21 +1,21 @@ // 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.mjs"; 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"; + import { useToast } from "@/hooks/use-toast"; @@ -24,200 +24,137 @@ import { useToast } from "@/hooks/use-toast"; import { Eye, EyeOff, Mail, Lock, Loader2, UserCheck, Stethoscope, IdCard, Receipt } 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 300baf9..509af00 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,34 +61,47 @@ 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"); - 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 = () => { @@ -103,7 +114,7 @@ useEffect(() => { const menuItems = [ { - href: "#", + href: "/doctor/dashboard", icon: Home, label: "Dashboard", // Botão para o dashboard do médico @@ -126,6 +137,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) { @@ -133,10 +150,10 @@ useEffect(() => { } return ( -
- {/* Sidebar para desktop */} -
-
+ // O restante do seu código JSX permanece exatamente o mesmo +
+
+
{!sidebarCollapsed && (
@@ -151,7 +168,6 @@ useEffect(() => {
- - // ... (seu código anterior) - - {/* Sidebar para desktop */} -
-
-
- {!sidebarCollapsed && ( -
-
-
+ {/* Sidebar para desktop */} +
+
+
+ {!sidebarCollapsed && ( +
+
+
+
+ MediConnect
- MediConnect -
- )} - + )} + +
-
- -
-
- {/* Se a sidebar estiver recolhida, o avatar e o texto do usuário também devem ser condensados ou ocultados */} - {!sidebarCollapsed && ( - <> - +
+
+ {!sidebarCollapsed && ( + <> + + + + {doctorData.name + .split(" ") + .map((n) => n[0]) + .join("")} + + +
+

{doctorData.name}

+

{doctorData.specialty}

+
+ + )} + {sidebarCollapsed && ( + {doctorData.name @@ -218,40 +247,17 @@ useEffect(() => { .join("")} -
-

{doctorData.name}

-

{doctorData.specialty}

-
- - )} - {sidebarCollapsed && ( - {/* Centraliza o avatar quando recolhido */} - - - {doctorData.name - .split(" ") - .map((n) => n[0]) - .join("")} - - - )} -
+ )} +
- {/* Novo botão de sair, usando a mesma estrutura dos itens de menu */} -
- - {!sidebarCollapsed && Sair} +
+ + {!sidebarCollapsed && Sair} +
- -
- {isMobileMenuOpen && ( -
- )} + {isMobileMenuOpen &&
}
@@ -271,7 +277,7 @@ useEffect(() => { const isActive = pathname === item.href || (item.href !== "/" && pathname.startsWith(item.href)); return ( - {/* Fechar menu ao clicar */} +
{item.label} @@ -297,17 +303,22 @@ useEffect(() => {

{doctorData.specialty}

-
- - {/* Main Content */} -
- {/* Header */} +
@@ -326,11 +337,9 @@ useEffect(() => {
- {/* Page Content */}
{children}
- {/* Logout confirmation dialog */} @@ -350,4 +359,4 @@ useEffect(() => {
); -} \ No newline at end of file +} diff --git a/components/finance-layout.tsx b/components/finance-layout.tsx index 6fbde9c..d1904b7 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 24a719e..2af8ac9 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,80 +36,81 @@ 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: "/manager/dashboard/", icon: Home, label: "Dashboard" }, - { href: "#", icon: Calendar, label: "Relatórios gerenciais" }, - { href: "/manager/usuario/", icon: User, label: "Gestão de Usuários" }, - { href: "/manager/home", 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 && (
@@ -141,136 +128,79 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) { onClick={() => setSidebarCollapsed(!sidebarCollapsed)} className="p-1" > - {sidebarCollapsed ? ( - - ) : ( - - )} + {sidebarCollapsed ? : }
- {/* 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 653a693..7ac9415 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 7fd4835..5bd51fd 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 17f8a0c..548c86a 100644 --- a/services/api.mjs +++ b/services/api.mjs @@ -1,12 +1,69 @@ +// Caminho: [seu-caminho]/services/api.mjs + const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co"; +const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; + +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", + "apikey": API_KEY, + }, + body: JSON.stringify({ email, password }), + }); + + const data = await response.json(); + + 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; +} + +// --- 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 = {}) { + const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null; + + const headers = { + "Content-Type": "application/json", + "apikey": API_KEY, + ...(token ? { "Authorization": `Bearer ${token}` } : {}), + ...options.headers, + }; const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; 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 +77,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 +99,69 @@ 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) { + let errorBody; + try { + errorBody = await response.json(); + } catch (e) { + errorBody = await response.text(); + } + 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, 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 +}; 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 +178,4 @@ export const api = { request(endpoint, { method: "PATCH", body: JSON.stringify(data) }), delete: (endpoint) => request(endpoint, { method: "DELETE" }), }; + diff --git a/services/availabilityApi.mjs b/services/availabilityApi.mjs new file mode 100644 index 0000000..40b7fcf --- /dev/null +++ b/services/availabilityApi.mjs @@ -0,0 +1,9 @@ +import { api } from "./api.mjs"; + +export const AvailabilityService = { + 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 new file mode 100644 index 0000000..62e893e --- /dev/null +++ b/services/exceptionApi.mjs @@ -0,0 +1,8 @@ +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}`), }; 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");