@@ -272,8 +330,8 @@ function PatientEdit() {
@@ -281,8 +339,8 @@ function PatientEdit() {
@@ -303,8 +361,8 @@ function PatientEdit() {
@@ -320,15 +378,15 @@ function PatientEdit() {
@@ -382,27 +440,27 @@ function PatientEdit() {
@@ -417,15 +475,15 @@ function PatientEdit() {
@@ -433,7 +491,7 @@ function PatientEdit() {
-
@@ -467,27 +525,27 @@ function PatientEdit() {
@@ -495,35 +553,35 @@ function PatientEdit() {
);
-};
+}
export default PatientEdit;
diff --git a/src/components/forms/AgendaForm.jsx b/src/components/forms/AgendaForm.jsx
new file mode 100644
index 0000000..bc38983
--- /dev/null
+++ b/src/components/forms/AgendaForm.jsx
@@ -0,0 +1,250 @@
+import "../../assets/css/index.css"
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import { getAccessToken } from "../../utils/auth.js";
+import { getUserRole } from "../../utils/userInfo.js";
+
+function AgendaForm() {
+ const [doctors, setDoctors] = useState([]);
+ const [doctorId, setDoctorId] = useState("");
+ const [weekday, setWeekday] = useState("");
+ const [startTime, setStartTime] = useState("");
+ const [endTime, setEndTime] = useState("");
+ const [appointmentType, setAppointmentType] = useState("presencial");
+ const [active, setActive] = useState(true);
+ const [loading, setLoading] = useState(true);
+ const role = getUserRole();
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || "https://yuanqfswhberkoevtmfr.supabase.co";
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
+
+ const navigate = useNavigate();
+ const tokenUsuario = getAccessToken();
+
+ const headers = {
+ apikey:
+ supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ "Content-Type": "application/json",
+ };
+
+ // Buscar médicos
+ useEffect(() => {
+ setLoading(true);
+ fetch(`${supabaseUrl}/rest/v1/doctors`, {
+ headers,
+ })
+ .then(async (res) => {
+ if (!res.ok) {
+ const text = await res.text();
+ throw new Error(`Erro ${res.status}: ${text}`);
+ }
+ return res.json();
+ })
+ .then((data) => {
+ console.log("Médicos carregados:", data);
+ setDoctors(Array.isArray(data) ? data : []);
+ })
+ .catch((err) => {
+ console.error("Erro ao carregar médicos:", err);
+ setDoctors([]);
+ })
+ .finally(() => setLoading(false));
+ }, []);
+
+ // Criar disponibilidade
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!doctorId || !weekday || !startTime || !endTime) {
+ alert("Preencha todos os campos obrigatórios!");
+ return;
+ }
+
+ // ✅ Tenta pegar o ID do usuário logado, se existir
+ // (caso o tokenUsuario contenha JWT com o UUID do usuário)
+ let createdBy = null;
+ try {
+ const payload = JSON.parse(atob(tokenUsuario.split(".")[1]));
+ createdBy = payload?.sub || null;
+ } catch (error) {
+ console.warn("Token inválido ou sem UUID. Usando null para created_by.");
+ }
+
+ const body = {
+ doctor_id: doctorId,
+ weekday,
+ start_time: startTime,
+ end_time: endTime,
+ slot_minutes: 30,
+ appointment_type: appointmentType,
+ active,
+ created_by: createdBy, // ✅ Envia null se não houver UUID válido
+ };
+
+ console.log("Enviando agenda:", body);
+
+ fetch(
+ `${supabaseUrl}/rest/v1/doctor_availability`,
+ {
+ method: "POST",
+ headers,
+ body: JSON.stringify(body),
+ }
+ )
+ .then(async (res) => {
+ const text = await res.text();
+ if (!res.ok) {
+ throw new Error(`Erro ${res.status}: ${text}`);
+ }
+ return text ? JSON.parse(text) : {};
+ })
+ .then(() => {
+ alert("✅ Agenda criada com sucesso!");
+ navigate(`/${role}/agendadoctor`);
+ })
+ .catch((err) => {
+ console.error("❌ Erro ao criar agenda:", err);
+ alert("Erro ao criar agenda. Veja o console para mais detalhes.");
+ });
+ };
+
+ return (
+
+ );
+}
+
+export default AgendaForm;
diff --git a/src/components/forms/ConsultaForm.jsx b/src/components/forms/ConsultaForm.jsx
new file mode 100644
index 0000000..48a157b
--- /dev/null
+++ b/src/components/forms/ConsultaForm.jsx
@@ -0,0 +1,477 @@
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import Swal from "sweetalert2";
+import "../../assets/css/index.css";
+import { getAccessToken } from "../../utils/auth";
+import { getUserId } from "../../utils/userInfo";
+import { sendSMS } from "../../utils/sendSMS";
+import { getUserRole } from "../../utils/userInfo";
+import emailjs from 'emailjs-com';
+
+function ConsultaForm() {
+ const role = getUserRole();
+ const [minDate, setMinDate] = useState("");
+ const [pacientes, setPacientes] = useState([]);
+ const [medicos, setMedicos] = useState([]);
+ const [horariosDisponiveis, setHorariosDisponiveis] = useState([]);
+ const [apiResponse, setApiResponse] = useState(null);
+ const [carregandoHorarios, setCarregandoHorarios] = useState(false);
+ const tokenUsuario = getAccessToken();
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || "https://yuanqfswhberkoevtmfr.supabase.co";
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
+ const servicekey = import.meta.env.VITE_SERVICE_KEY
+ const templatekey = import.meta.env.VITE_TEMPLATE_KEY
+ const publickey = import.meta.env.VITE_PUBLIC_KEY
+
+ const [formData, setFormData] = useState({
+ appointment_type: "presencial",
+ chief_complaint: "",
+ doctor_id: "",
+ duration_minutes: 30,
+ insurance_provider: "",
+ patient_id: "",
+ patient_notes: "",
+ scheduled_date: "",
+ scheduled_time: "",
+ });
+
+ const navigate = useNavigate();
+
+ // 🔹 Define a data mínima
+ useEffect(() => {
+ const today = new Date();
+ const offset = today.getTimezoneOffset();
+ today.setMinutes(today.getMinutes() - offset);
+ setMinDate(today.toISOString().split("T")[0]);
+ }, []);
+
+ // 🔹 Buscar pacientes
+ useEffect(() => {
+ const fetchPacientes = async () => {
+ try {
+ const response = await fetch(
+ `${supabaseUrl}/rest/v1/patients`,
+ {
+ headers: {
+ apikey: supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ }
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ setPacientes(data);
+ } else {
+ console.error("Erro ao buscar pacientes");
+ }
+ } catch (error) {
+ console.error("Erro:", error);
+ }
+ };
+
+ fetchPacientes();
+ }, []);
+
+ // 🔹 Buscar médicos
+ useEffect(() => {
+ const fetchMedicos = async () => {
+ try {
+ const response = await fetch(
+ `${supabaseUrl}/rest/v1/doctors`,
+ {
+ headers: {
+ apikey: supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ }
+ );
+
+ if (response.ok) {
+ const data = await response.json();
+ setMedicos(data);
+ } else {
+ console.error("Erro ao buscar médicos");
+ }
+ } catch (error) {
+ console.error("Erro:", error);
+ }
+ };
+
+ fetchMedicos();
+ }, []);
+
+ // 🔹 Buscar horários disponíveis
+ const fetchHorariosDisponiveis = async (doctorId, date, appointmentType) => {
+ if (!doctorId || !date) {
+ setHorariosDisponiveis([]);
+ setApiResponse(null);
+ return;
+ }
+
+ setCarregandoHorarios(true);
+
+ const startDate = `${date}T00:00:00.000Z`;
+ const endDate = `${date}T23:59:59.999Z`;
+
+ const payload = {
+ doctor_id: doctorId,
+ start_date: startDate,
+ end_date: endDate,
+ appointment_type: appointmentType || "presencial",
+ };
+
+ console.log("🚀 AgendaForm - Payload enviado para get-available-slots:", payload);
+ console.log("🔑 AgendaForm - Token do usuário:", tokenUsuario ? "EXISTS" : "NULL");
+
+ try {
+ const response = await fetch(
+ `${supabaseUrl}/functions/v1/get-available-slots`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ apikey: supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ body: JSON.stringify(payload),
+ }
+ );
+
+ const data = await response.json();
+ setApiResponse(data);
+
+ console.log("🔍 AgendaForm (Admin) - Resposta da Edge Function:", data);
+
+ if (!response.ok) throw new Error(data.error || "Erro ao buscar horários");
+
+ const slotsDisponiveis = (data?.slots || []).filter((s) => s.available);
+
+ console.log("✅ AgendaForm (Admin) - Slots disponíveis após filtro:", slotsDisponiveis);
+ console.log("🔍 AgendaForm (Admin) - Todos os slots (antes do filtro):", data?.slots);
+ console.log("❌ AgendaForm (Admin) - Slots NÃO disponíveis:", (data?.slots || []).filter((s) => !s.available));
+
+ setHorariosDisponiveis(slotsDisponiveis);
+
+ if (slotsDisponiveis.length === 0)
+ Swal.fire("Atenção", "Nenhum horário disponível para este dia.", "info");
+ } catch (error) {
+ console.error("Erro ao buscar horários disponíveis:", error);
+ setHorariosDisponiveis([]);
+ setApiResponse(null);
+ Swal.fire("Erro", "Não foi possível obter os horários disponíveis.", "error");
+ } finally {
+ setCarregandoHorarios(false);
+ }
+ };
+
+ // 🔹 Atualiza campos
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({ ...prev, [name]: value }));
+ };
+
+ // 🔹 Atualiza horários quando médico ou data mudam
+ useEffect(() => {
+ if (formData.doctor_id && formData.scheduled_date) {
+ fetchHorariosDisponiveis(
+ formData.doctor_id,
+ formData.scheduled_date,
+ formData.appointment_type
+ );
+ }
+ }, [formData.doctor_id, formData.scheduled_date, formData.appointment_type]);
+
+ // 🔹 Envia formulário
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!formData.scheduled_date || !formData.scheduled_time) {
+ Swal.fire("Atenção", "Selecione uma data e horário válidos", "warning");
+ return;
+ }
+
+ const scheduled_at = `${formData.scheduled_date}T${formData.scheduled_time}:00Z`;
+
+ const payload = {
+ patient_id: formData.patient_id,
+ doctor_id: formData.doctor_id,
+ scheduled_at,
+ duration_minutes: formData.duration_minutes,
+ appointment_type: formData.appointment_type,
+ chief_complaint: formData.chief_complaint,
+ patient_notes: formData.patient_notes,
+ insurance_provider: formData.insurance_provider,
+ created_by: getUserId(),
+ };
+
+ try {
+ const response = await fetch(
+ `${supabaseUrl}/rest/v1/appointments`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ apikey: supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ Prefer: "return=representation",
+ },
+ body: JSON.stringify(payload),
+ }
+ );
+
+ if (response.ok) {
+ const consultaCriada = await response.json();
+
+ // 🔹 Busca o telefone do paciente selecionado
+ const pacienteSelecionado = pacientes.find(
+ (p) => String(p.id) === String(formData.patient_id)
+ );
+ const email =
+ pacienteSelecionado?.email ||
+ null;
+
+ if (email){
+ try {
+ await emailjs.send(
+ `${servicekey}`,
+ `${templatekey}`,
+ {
+ nome_do_paciente: pacienteSelecionado.name || pacienteSelecionado.full_name || `Paciente #${pacienteSelecionado.id}`,
+ date: formData.scheduled_date,
+ time: formData.scheduled_time,
+ doctor_name: medicos.find((m) => String(m.id) === String(formData.doctor_id))?.full_name ||
+ medicos.find((m) => String(m.id) === String(formData.doctor_id))?.full_name ||
+ medicos.find((m) => String(m.id) === String(formData.doctor_id))?.doctor_name ||
+ `Médico #${formData.doctor_id}`,
+ email: email,
+ },
+ `${publickey}`
+ );
+ console.log("Email de confirmação enviado com sucesso!");
+ } catch (error) {
+ console.error("Erro ao enviar email de confirmação:", error);
+ }
+ }
+ Swal.fire({
+ title: "Sucesso!",
+ text: "Consulta criada com sucesso!",
+ icon: "success",
+ confirmButtonText: "OK",
+ }).then(() => {
+ navigate(`/${role}/consultalist`);
+ });
+ } else {
+ const error = await response.json();
+ console.error(error);
+ Swal.fire("Erro", "Não foi possível criar a consulta", "error");
+ }
+ } catch (error) {
+ console.error(error);
+ Swal.fire("Erro", "Erro de conexão com o servidor", "error");
+ }
+ };
+
+ return (
+
+
+
+
+
Nova consulta
+
+ Informações do paciente
+
+
+
+
+
+
+ );
+}
+
+export default ConsultaForm;
\ No newline at end of file
diff --git a/src/components/forms/DoctorForm.jsx b/src/components/forms/DoctorForm.jsx
new file mode 100644
index 0000000..e92e2c2
--- /dev/null
+++ b/src/components/forms/DoctorForm.jsx
@@ -0,0 +1,488 @@
+import "../../assets/css/index.css";
+import { withMask } from "use-mask-input";
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import Swal from "sweetalert2";
+import { getAccessToken } from "../../utils/auth.js";
+import { getUserRole } from "../../utils/userInfo.js";
+const role = getUserRole();
+
+
+function DoctorForm() {
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || "https://yuanqfswhberkoevtmfr.supabase.co";
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
+
+ const role = getUserRole();
+ const [doctorData, setDoctorData] = useState({
+ full_name: "",
+ cpf: "",
+ email: "",
+ crm: "",
+ crm_uf: "",
+ specialty: "",
+ birth_date: "",
+ phone_mobile: "",
+ cep: "",
+ street: "",
+ number: "",
+ complement: "",
+ neighborhood: "",
+ city: "",
+ state: "",
+ active: false,
+ });
+
+ const tokenUsuario = getAccessToken();
+ const navigate = useNavigate();
+
+ const estados = {
+ AC: "Acre", AL: "Alagoas", AP: "Amapá", AM: "Amazonas",
+ BA: "Bahia", CE: "Ceará", DF: "Distrito Federal", ES: "Espírito Santo",
+ GO: "Goiás", MA: "Maranhão", MT: "Mato Grosso", MS: "Mato Grosso do Sul",
+ MG: "Minas Gerais", PA: "Pará", PB: "Paraíba", PR: "Paraná",
+ PE: "Pernambuco", PI: "Piauí", RJ: "Rio de Janeiro", RN: "Rio Grande do Norte",
+ RS: "Rio Grande do Sul", RO: "Rondônia", RR: "Roraima", SC: "Santa Catarina",
+ SP: "São Paulo", SE: "Sergipe", TO: "Tocantins"
+ };
+
+ const buscarCep = () => {
+ const cep = doctorData.cep.replace(/\D/g, "");
+ if (cep.length === 8) {
+ fetch(`https://brasilapi.com.br/api/cep/v2/${cep}`)
+ .then((response) => response.json())
+ .then((data) => {
+ setDoctorData((prev) => ({
+ ...prev,
+ city: data.city || '',
+ street: data.street || '',
+ neighborhood: data.neighborhood || '',
+ state: estados[data.state] || data.state
+ }));
+ })
+ .catch(() => {
+ Swal.fire({ title: "Erro ao buscar CEP", icon: "error" });
+ });
+ }
+ };
+
+ const handleChange = (e) => {
+ const { name, value, type, checked } = e.target;
+ setDoctorData((prev) => ({
+ ...prev,
+ [name]: type === "checkbox" ? checked : value,
+ }));
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ const requiredFields = [
+ "full_name", "cpf", "email", "phone_mobile", "crm", "crm_uf",
+ "specialty", "birth_date", "cep", "street", "number",
+ "neighborhood", "city", "state"
+ ];
+
+ const missingFields = requiredFields.filter(
+ (field) => !doctorData[field] || doctorData[field].toString().trim() === ""
+ );
+
+ if (missingFields.length > 0) {
+ Swal.fire({
+ title: "Campos obrigatórios faltando",
+ text: "Por favor, preencha todos os campos antes de continuar.",
+ icon: "warning"
+ });
+ return;
+ }
+
+ try {
+ // === 2️⃣ ETAPA 1: CRIAR O USUÁRIO NO AUTH (CHAMANDO A FUNCTION) ===
+ const authHeaders = new Headers();
+ authHeaders.append("apikey", supabaseAK);
+ authHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+ authHeaders.append("Content-Type", "application/json");
+ const authRaw = JSON.stringify({
+ ...doctorData,
+ password: "12345678", // Usando CRM como senha
+ role: "medico"
+ });
+
+ console.log("📤 Body enviado para Auth:", authRaw);
+ console.log("🌐 Endpoint:", `${supabaseUrl}/functions/v1/create-user-with-password`);
+
+ const authResponse = await fetch(
+ `${supabaseUrl}/functions/v1/create-user-with-password`,
+ {
+ method: 'POST',
+ headers: authHeaders,
+ body: authRaw,
+ redirect: 'follow'
+ }
+ );
+ console.log("📥 Status da resposta:", authResponse.status, authResponse.statusText);
+ if (!authResponse.ok) {
+ console.log("❌ Resposta não OK de criação de usuário no Auth");
+ }else{
+ navigate(`/${role}/doctorlist`)
+ }
+ } catch (error) {
+ console.error("❌ Erro no cadastro em duas etapas:", error);
+ Swal.fire({
+ title: "Erro ao cadastrar",
+ text: error.message, // Exibe a mensagem de erro específica
+ icon: "error"
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default DoctorForm;
\ No newline at end of file
diff --git a/src/components/forms/LaudoForm.jsx b/src/components/forms/LaudoForm.jsx
new file mode 100644
index 0000000..0e9d506
--- /dev/null
+++ b/src/components/forms/LaudoForm.jsx
@@ -0,0 +1,445 @@
+
+import { useEditor, EditorContent } from '@tiptap/react'
+import StarterKit from '@tiptap/starter-kit'
+import Image from '@tiptap/extension-image';
+import { useState, useEffect, useRef } from 'react';
+import { Card, Collapse } from "react-bootstrap"; // <-- IMPORT CORRETO
+import { ChevronDown, ChevronUp } from "lucide-react";
+import { getAccessToken } from '../../utils/auth';
+import Select from 'react-select';
+import Swal from 'sweetalert2';
+import { useNavigate } from 'react-router-dom';
+import { FaMicrophone } from "react-icons/fa";
+import { InterimMark } from '../../utils/InterimMark'; // <-- Verifique se esse caminho está certo!
+import { getUserRole, getDoctorId } from '../../utils/userInfo';
+
+
+function Bar({ comandos, handleSubmit, toggleRecording, isRecording }) {
+ const inputRef = useRef(null);
+
+ const handleAbrirExplorador = () => {
+ inputRef.current.click(); // abre o explorador
+ };
+
+ const handleArquivoSelecionado = (event) => {
+ const arquivo = event.target.files[0];
+ if (arquivo) {
+ const imageUrl = URL.createObjectURL(arquivo);
+ comandos.agregarImagen(imageUrl);
+ event.target.value = null;
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+ {/*
*/}
+
+
+
+
+
+
+ <>
+
+
+
+ >
+
+
+ {/*
*/}
+
+
+
+
+
+ >
+ );
+};
+
+function LaudoForm() {
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || "https://yuanqfswhberkoevtmfr.supabase.co";
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
+
+ const navigate = useNavigate();
+ const [paciente, setPaciente] = useState([]);
+ const tokenUsuario = getAccessToken()
+ var myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+ var requestOptions = {
+ method: 'GET',
+ headers: myHeaders,
+ redirect: 'follow'
+ };
+ useEffect(() => {
+ fetch(`${supabaseUrl}/rest/v1/patients`, requestOptions)
+ .then(response => response.json())
+ .then(result => setPaciente(Array.isArray(result) ? result : []))
+ .catch(error => console.log('error', error));
+ }, [])
+ const options = paciente.map(p => ({
+ value: p.id,
+ label: p.full_name
+ }));
+ function gerarOrderNumber() {
+ const prefixo = "REL";
+
+ const agora = new Date();
+ const ano = agora.getFullYear();
+ const mes = String(agora.getMonth() + 1).padStart(2, "0"); // adiciona 0 à esquerda se necessário
+
+ // Gerar um código aleatório de 6 caracteres (letras maiúsculas + números)
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
+ let codigo = "";
+ for (let i = 0; i < 6; i++) {
+ codigo += chars.charAt(Math.floor(Math.random() * chars.length));
+ }
+
+ return `${prefixo}-${ano}-${mes}-${codigo}`;
+ }
+
+ // Exemplo de uso:
+ const orderNumber = gerarOrderNumber();
+ const [laudos, setLaudos] = useState({
+ patient_id: "",
+ order_number: "",
+ exam: "",
+ diagnosis: "",
+ conclusion: "",
+ cid_code: "",
+ content_html: "",
+ status: "draft",
+ requested_by: getDoctorId(),
+ });
+ const handlePacienteChange = (selected) => {
+ setLaudos(prev => ({
+ ...prev,
+ patient_id: selected ? selected.value : ""
+ }));
+ };
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setLaudos((prev) => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleSubmit = (e) => {
+ const role = getUserRole();
+ e.preventDefault();
+ if (!laudos.patient_id || !laudos.diagnosis || !laudos.exam || !laudos.conclusion) {
+ Swal.fire({
+ title: "Por favor, preencha todos os campos obrigatórios.",
+ icon: "warning",
+ draggable: true
+ });
+ return;
+ }
+ var myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+ myHeaders.append("Content-Type", "application/json");
+
+ var raw = JSON.stringify(laudos);
+
+ var requestOptions = {
+ method: 'POST',
+ headers: myHeaders,
+ body: raw,
+ redirect: 'follow'
+ };
+ fetch(`${supabaseUrl}/rest/v1/reports`, requestOptions)
+ .then(response => response.text())
+ .then(result => {
+ console.log(result);
+ Swal.fire({
+ title: "Laudo adicionado!",
+ icon: "success",
+ draggable: true
+ });
+ navigate(`/${role}/laudolist`);
+ })
+ .catch(error => console.log('error', error));
+ };
+
+ const [open, setOpen] = useState(false);
+ const editor = useEditor({
+ extensions: [StarterKit, Image, InterimMark ],
+ content: "",
+ onUpdate: ({ editor }) => {
+ setLaudos(prev => ({
+ ...prev,
+ content_html: editor.getHTML()
+ }));
+ }
+ })
+ const [isRecording, setIsRecording] = useState(false);
+ const recognitionRef = useRef(null);
+ const lastInsertedRef = useRef({ from: -1, to: -1, text: '' });
+
+
+ useEffect(() => {
+ const SpeechRecognition =
+ window.SpeechRecognition || window.webkitSpeechRecognition;
+
+ if (!SpeechRecognition) {
+ alert("Seu navegador não suporta reconhecimento de voz 😢");
+ return;
+ }
+
+ const recognition = new SpeechRecognition();
+ recognition.lang = "pt-BR";
+ recognition.continuous = false;
+ recognition.interimResults = true;
+
+ recognition.onresult = (event) => {
+ if (!editor) return;
+
+ const result = event.results[0];
+ const transcript = result[0].transcript;
+ const last = lastInsertedRef.current;
+
+ // --- CORREÇÃO DE LÓGICA ---
+ // Vamos rodar a deleção como um comando SEPARADO primeiro.
+ if (last.from !== -1 &&
+ editor.state.doc.textBetween(last.from, last.to) === last.text)
+ {
+ // Roda a deleção e PARA.
+ editor.chain().focus()
+ .deleteRange({ from: last.from, to: last.to })
+ .run();
+ }
+
+ // Pega a posição ATUAL (depois da deleção)
+ const currentPos = editor.state.selection.from;
+
+ if (result.isFinal) {
+ // --- RESULTADO FINAL (PRETO) ---
+ // Roda a inserção final como um comando SEPARADO.
+ editor.chain().focus()
+ .insertContent(transcript + ' ')
+ .run();
+
+ // Reseta a Ref
+ lastInsertedRef.current = { from: -1, to: -1, text: '' };
+
+ } else {
+ // --- RESULTADO PROVISÓRIO (CINZA) ---
+ // Esta é a nova estratégia: "Ligar" a mark, inserir, "Desligar" a mark.
+ // Roda tudo como um comando SEPARADO.
+ editor.chain()
+ .focus()
+ .setMark('interimMark') // <-- "Pincel cinza" LIGADO
+ .insertContent(transcript) // <-- Insere o texto
+ .unsetMark('interimMark') // <-- "Pincel cinza" DESLIGADO
+ .run();
+
+ // Atualiza a Ref com a posição do texto cinza
+ lastInsertedRef.current = {
+ from: currentPos,
+ to: currentPos + transcript.length,
+ text: transcript
+ };
+ }
+ // Não precisamos mais do 'editorChain.run()' aqui embaixo
+ };
+
+ recognition.onerror = (err) => {
+ // ... (código do onerror sem mudanças)
+ };
+
+ recognition.onend = () => {
+ // ... (código do onend sem mudanças)
+ };
+
+ recognitionRef.current = recognition;
+
+ return () => {
+ recognition.stop();
+ };
+
+ }, [editor, isRecording]);
+
+ const toggleRecording = () => {
+ if (!recognitionRef.current) return;
+
+ if (isRecording) {
+ // Usuário clicou para PARAR
+ setIsRecording(false); // <-- Seta o estado
+ recognitionRef.current.stop(); // <-- Para a API
+ // O 'onend' será chamado e fará a limpeza/confirmação.
+ } else {
+ // Usuário clicou para COMEÇAR
+ editor?.chain().focus().run();
+ setIsRecording(true); // <-- Seta o estado
+ recognitionRef.current.start(); // <-- Inicia a API
+ }
+ };
+
+ const comandos = {
+ toggleBold: () => editor.chain().focus().toggleBold().run(),
+ toggleItalic: () => editor.chain().focus().toggleItalic().run(),
+ toggleUnderline: () => editor.chain().focus().toggleUnderline().run(),
+ toggleCodeBlock: () => editor.chain().focus().toggleCodeBlock().run(),
+ toggleH1: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
+ toggleH2: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
+ toggleH3: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
+ toggleParrafo: () => editor.chain().focus().setParagraph().run(),
+ toggleListaOrdenada: () => editor.chain().focus().toggleOrderedList().run(),
+ toggleListaPuntos: () => editor.chain().focus().toggleBulletList().run(),
+ agregarImagen: (url) => {
+ if (!url) return;
+ editor.chain().focus().setImage({ src: url }).run();
+ },
+ agregarLink: () => {
+ const url = window.prompt('URL do link')
+ if (url) {
+ editor.chain().focus().setLink({ href: url }).run()
+ }
+ }
+ }
+
+ return (
+
+ );
+}
+
+export default LaudoForm;
+;
\ No newline at end of file
diff --git a/src/pages/Patient/Patientform.jsx b/src/components/forms/PatientForm.jsx
similarity index 51%
rename from src/pages/Patient/Patientform.jsx
rename to src/components/forms/PatientForm.jsx
index eeb8a1f..bb7a554 100644
--- a/src/pages/Patient/Patientform.jsx
+++ b/src/components/forms/PatientForm.jsx
@@ -1,274 +1,179 @@
-import { useState, useEffect,useRef } from "react";
-import "../../assets/css/index.css"
+import { useState, useEffect, useRef } from "react";
+import "../../assets/css/index.css";
import { withMask } from "use-mask-input";
-import supabase from "../../Supabase"
+import supabase from "../../Supabase.js";
import { useNavigate } from "react-router-dom";
+import { getAccessToken } from "../../utils/auth.js";
+const AvatarForm = "/img/AvatarForm.jpg";
+const AnexoDocumento = "/img/AnexoDocumento.png";
+import Swal from "sweetalert2";
+import { getUserRole } from "../../utils/userInfo.js";
+
function Patientform() {
+ const role = getUserRole();
+ const tokenUsuario = getAccessToken();
const [patientData, setpatientData] = useState({
- nome: "",
- nome_social: "",
+ full_name: "",
cpf: "",
- rg: "",
- outros_documentos: "",
- numero_documento: "",
- estado_civil: "",
- raça: "",
- data_nascimento: null,
- profissao: "",
- nome_pai: "",
- profissao_pai: "",
- nome_mae: "",
- profissao_mae: "",
- nome_responsavel: "",
- codigo_legado: "",
- foto_url: "",
- rn: "false",
- sexo: "",
- celular: "",
email: "",
- telefone1: "",
- telefone2: "",
+ birth_date: "",
+ social_name: "",
+ sex: "",
+ street: "",
+ number: "",
+ neighborhood: "",
+ city: "",
+ state: "",
cep: "",
- estado: "",
- logradouro: "",
- bairro: "",
- numero: "",
- complemento: "",
- referencia: "",
- status: "inativo",
- observaçao: ""
-
-
- })
-
+ ethnicity: "",
+ father_name: "",
+ father_profession: "",
+ legacy_code: "",
+ marital_status: "",
+ mother_name: "",
+ mother_profession: "",
+ phone1: "",
+ phone2: "",
+ phone_mobile: "",
+ profession: "",
+ reference: "",
+ guardian_cpf: "",
+ guardian_name: "",
+ complement: "",
+ });
+ const [previewUrl, setPreviewUrl] = useState(AvatarForm);
const [fotoFile, setFotoFile] = useState(null);
const fileRef = useRef(null);
+ const navigate = useNavigate();
useEffect(() => {
- console.log("Estado atualizado:", patientData);
- }, [patientData]);
-
- // aqui eu fiz uma funçao onde atualiza o estado do paciente, eu poderia ir mudando com o onchange em cada input mas assim ficou melhor
- // e como se fosse 'onChange={(e) => setpatientData({ ...patientData, rg: e.target.value })}'
- // prev= pega o valor anterior
+ console.log("Estado atualizado:", patientData);
+ }, [patientData]);
+
const handleChange = (e) => {
const { name, value } = e.target;
setpatientData((prev) => ({
...prev,
- [name]: value
+ [name]: value,
}));
};
- // aqui esta sentando os valores nos inputs
- const setValuesFromCep = (data) => {
- document.getElementById('logradouro').value = data.logradouro || '';
- document.getElementById('bairro').value = data.bairro || '';
- document.getElementById('cidade').value = data.localidade || '';
- document.getElementById('estado').value = data.uf || '';
- }
- const buscarCep = (e) => {
- const cep = patientData.cep.replace(/\D/g, '');
- console.log(cep);
- fetch(`https://viacep.com.br/ws/${cep}/json/`)
- .then(response => response.json())
- .then(data => {
- console.log(data)
- // salvando os valores para depois colocar nos inputs
- setValuesFromCep(data)
- // estou salvando os valoeres no patientData
- setpatientData((prev) => ({
- ...prev,
- cidade: data.localidade || '',
- logradouro: data.logradouro || '',
- bairro: data.bairro || '',
- estado: data.estado || ''
- }));
- })
- }
- const navigate = useNavigate();
- // enviando para o supabase
+ const estados = {
+ AC: "Acre", AL: "Alagoas", AP: "Amapá", AM: "Amazonas", BA: "Bahia",
+ CE: "Ceará", DF: "Distrito Federal", ES: "Espírito Santo", GO: "Goiás",
+ MA: "Maranhão", MT: "Mato Grosso", MS: "Mato Grosso do Sul",
+ MG: "Minas Gerais", PA: "Pará", PB: "Paraíba", PR: "Paraná",
+ PE: "Pernambuco", PI: "Piauí", RJ: "Rio de Janeiro", RN: "Rio Grande do Norte",
+ RS: "Rio Grande do Sul", RO: "Rondônia", RR: "Roraima", SC: "Santa Catarina",
+ SP: "São Paulo", SE: "Sergipe", TO: "Tocantins"
+ };
+
+ const setValuesFromCep = (data) => {
+ setpatientData((prev) => ({
+ ...prev,
+ city: data.city || '',
+ street: data.street || '',
+ neighborhood: data.neighborhood || '',
+ state: estados[data.state] || data.state || '',
+ }));
+ };
+
+ const buscarCep = (e) => {
+ const cep = patientData.cep.replace(/\D/g, "");
+ if (cep.length !== 8) return;
+
+ fetch(`https://brasilapi.com.br/api/cep/v2/${cep}`)
+ .then((response) => response.json())
+ .then((data) => {
+ if (data && !data.errors) {
+ setValuesFromCep(data);
+ } else {
+ console.log("CEP não encontrado");
+ }
+ })
+ .catch(err => console.error("Erro ao buscar CEP:", err));
+ };
+
+
const handleSubmit = async (e) => {
e.preventDefault();
- //const cpfValido = await validarCpf(patientData.cpf);
-
- /*
- // Verifica se já existe paciente com o CPF
- const cpfExiste = await verificarCpfExistente(patientData.cpf);
- if (cpfExiste) {
- alert("Já existe um paciente cadastrado com este CPF!");
- return;
- }
- */
-
- // Calcula idade a partir da data de nascimento
- /*
- const hoje = new Date();
- const nascimento = new Date(patientData.data_nascimento);
- let idade = hoje.getFullYear() - nascimento.getFullYear();
- const m = hoje.getMonth() - nascimento.getMonth();
- if (m < 0 || (m === 0 && hoje.getDate() < nascimento.getDate())) {
- idade--;
- }
-
- let cpfRespValido = true;
- if (idade < 18) {
- cpfRespValido = await validarCpf(patientData.cpf_responsavel);
- }
-
- if (!cpfValido || !cpfRespValido) {
- console.log("CPF inválido. Não enviando o formulário.");
- // Não envia se algum CPF for inválido
- return;
- }*/
-
- // Campos obrigatórios
const requiredFields = [
- "nome",
- "cpf",
- "data_nascimento",
- "sexo",
- "celular",
- "cep",
- "logradouro",
- "numero",
- "bairro",
- "estado",
- "status",
- "email"
+ "full_name", "cpf", "birth_date", "sex", "phone_mobile",
+ "cep", "street", "number", "neighborhood", "state", "email"
];
- const missingFields = requiredFields.filter(
- (field) => !patientData[field] || patientData[field].toString().trim() === ""
- );
+ const missingFields = requiredFields.filter(
+ (field) => !patientData[field] || patientData[field].toString().trim() === ""
+ );
- if (missingFields.length > 0) {
- alert("Por favor, preencha todos os campos obrigatórios.");
- return;
- }
+ if (missingFields.length > 0) {
+ Swal.fire("Atenção", `Campos obrigatórios faltando: ${missingFields.join(", ")}`, "warning");
+ return;
+ }
- const myHeaders = new Headers();
- myHeaders.append("Authorization", "Bearer
");
- myHeaders.append("Content-Type", "application/json");
- const raw = JSON.stringify(patientData);
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || "https://yuanqfswhberkoevtmfr.supabase.co";
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
- var requestOptions = {
- method: 'POST',
- headers: myHeaders,
- body: raw,
- redirect: 'follow'
- };
-
- fetch("https://mock.apidog.com/m1/1053378-0-default/pacientes", requestOptions)
- .then(response => response.json())
- .then(async (result) => {
- setpatientData(result)
- console.log(result)
-
- if (fotoFile && result?.id) {
- const formData = new FormData();
- formData.append("foto", fotoFile);
-
- try {
- const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${result.id}/foto`, {
- method: "POST",
- headers: {
- "Authorization": "Bearer "
- },
- body: formData
- });
- const uploadResult = await res.json();
- console.log("Foto enviada com sucesso:", uploadResult);
- } catch (error) {
- console.error("Erro no upload da foto:", error);
- }
- }
-
- if (patientData.documentoFile && result?.id) {
- const formData = new FormData();
- formData.append("anexo", patientData.documentoFile);
-
- try {
- const resAnexo = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${result.id}/anexos`, {
- method: "POST",
- headers: {
- "Authorization": "Bearer "
- },
- body: formData
- });
- const novoAnexo = await resAnexo.json();
- console.log("Anexo enviado com sucesso:", novoAnexo);
-
-
- setpatientData((prev) => ({
- ...prev,
- anexos: [...(prev.anexos || []), novoAnexo]
- }));
- } catch (error) {
- console.error("Erro no upload do anexo:", error);
- }
- }
-
- alert("paciente cadastrado")
- navigate("/patientlist")
- })
- .catch(error => console.log('error', error));
- /*console.log(patientData);
- const{data, error} = await supabase
- .from("Patient")
- .insert([patientData])
- .select()
- if(error){
- console.log("Erro ao inserir paciente:", error);
- }else{
- console.log("Paciente inserido com sucesso:", data);
- }*/
- };
-
- const validarCpf = async (cpf) => {
- const cpfLimpo = cpf.replace(/\D/g, "");
try {
- const response = await fetch("https://mock.apidog.com/m1/1053378-0-default/pacientes/validar-cpf", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({ cpf: cpfLimpo })
+ const headers = new Headers();
+ headers.append("apikey",
+ supabaseAK
+ );
+ headers.append("Authorization", `Bearer ${tokenUsuario}`);
+ headers.append("Content-Type", "application/json");
+
+ const raw = JSON.stringify({
+ ...patientData,
+ password: "12345678", // senha padrão
+ role: "paciente", // role padrão
+ create_patient_record: true
});
- const data = await response.json();
- if (data.valido === false) {
- alert("CPF inválido!");
- return false;
- } else if (data.valido === true) {
- return true;
- } else {
- return false;
+
+ console.log("📤 Enviando dados:", raw);
+
+ const response = await fetch(
+ `${supabaseUrl}/functions/v1/create-user-with-password`,
+ {
+ method: "POST",
+ headers,
+ body: raw,
+ redirect: "follow"
+ }
+ );
+
+ console.log("📥 Status:", response.status, response.statusText);
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(errorText || "Erro ao cadastrar paciente");
}
+
+ Swal.fire({
+ title: "Paciente cadastrado com sucesso!",
+ icon: "success",
+ });
+
+ navigate(`/${role}/patientlist`);
+
} catch (error) {
- alert("Erro ao validar CPF.");
- return false;
+ console.error("❌ Erro no cadastro:", error);
+ Swal.fire({
+ title: "Erro ao cadastrar",
+ text: error.message,
+ icon: "error",
+ });
}
};
-
- const verificarCpfExistente = async (cpf) => {
- const cpfLimpo = cpf.replace(/\D/g, "");
- try {
- const response = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes?cpf=${cpfLimpo}`);
- const data = await response.json();
- // Ajuste conforme o formato de resposta da sua API
- if (data && data.data && data.data.length > 0) {
- return true; // Já existe paciente com esse CPF
- }
- return false;
- } catch (error) {
- console.log("Erro ao verificar CPF existente.");
- return false;
+ const handleFileChange = (e) => {
+ const file = e.target.files[0];
+ setFotoFile(file);
+ if (file) {
+ setPreviewUrl(URL.createObjectURL(file));
}
};
-
return (
@@ -289,50 +194,44 @@ function Patientform() {
-

+
{
- // Remove no frontend
- setpatientData(prev => ({ ...prev, foto_url: "" }));
- setFotoFile(null); // Limpa no preview
+ setpatientData((prev) => ({
+ ...prev,
+ foto_url: "",
+ }));
+ setFotoFile(null);
+ setPreviewUrl(AvatarForm);
if (fileRef.current) fileRef.current.value = null;
-
- // Remove no backend e mostra resposta no console
- if (patientData.id) {
- try {
- const response = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientData.id}/foto`, {
- method: "DELETE",
- });
- const data = await response.json();
- console.log("Resposta da API ao remover foto:", data);
- if (response.ok || response.status === 200) {
- console.log("Foto removida com sucesso na API.");
- }
- } catch (error) {
- console.log("Erro ao remover foto:", error);
- }
- } else {
- console.log("Ainda não existe paciente cadastrado para remover foto na API.");
- }
}}
- >
+ >
Limpar
-
+
@@ -342,27 +241,34 @@ function Patientform() {
-
+
-
+
-
-
+
-
+
-
+
+
-
+
-
+
-
+
-
+
-
-
+
+
+ {/* Contato */}
Contato
+
+
+ {/* Endereço */}
Endereço
@@ -591,8 +576,11 @@ function Patientform() {
+
+
-
+
-
-
+
-
-
-

-
-
-
-
- {
- const file = e.target.files[0];
- if (file) {
- setpatientData((prev) => ({ ...prev, documentoFile: file }));
- }
- }}
- />
-
-
-
- {patientData.anexos?.length > 0 && (
-
- )}
-
-
-
- {/* Lista anexos */}
- {patientData.anexos?.length > 0 &&
- patientData.anexos.map((anexo) => (
-
- ))}
-
-
-
-
-
-
+
@@ -788,7 +732,9 @@ function Patientform() {
- );
-};
-export default Patientform;
\ No newline at end of file
+ );
+}
+
+export default Patientform;
+
diff --git a/src/components/layouts/Navbar.jsx b/src/components/layouts/Navbar.jsx
new file mode 100644
index 0000000..752af05
--- /dev/null
+++ b/src/components/layouts/Navbar.jsx
@@ -0,0 +1,465 @@
+import { useEffect, useRef, useState } from "react";
+import { useNavigate, useLocation, Link } from "react-router-dom";
+import "../../assets/css/index.css";
+import { logoutUser } from "../../Supabase";
+import Swal from "sweetalert2";
+import { getUserRole, clearUserInfo, getUserId } from "../../utils/userInfo";
+import { getAccessToken } from "../../utils/auth";
+
+const AvatarForm = "/img/AvatarForm.jpg";
+
+const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || "https://yuanqfswhberkoevtmfr.supabase.co";
+const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
+
+var myHeaders = new Headers();
+const tokenUsuario = getAccessToken();
+
+const LS_KEYS = {
+ dark: "pref_dark_mode",
+ daltonism: "pref_daltonism",
+ font: "pref_font_scale",
+};
+
+myHeaders.append("apikey", supabaseAK);
+myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+
+function Navbar({ onMenuClick }) {
+ const location = useLocation();
+ const navigate = useNavigate();
+
+ // Estados
+ const [openNotif, setOpenNotif] = useState(false);
+ const [openProfile, setOpenProfile] = useState(false);
+ const [previewUrl, setPreviewUrl] = useState(AvatarForm);
+ const [darkMode, setDarkMode] = useState(false);
+
+ // Estado para dados do utilizador
+ const [userData, setUserData] = useState({
+ email: "Carregando...",
+ role: "",
+ lastSignIn: ""
+ });
+
+ const notifRef = useRef(null);
+ const profileRef = useRef(null);
+ const fileRef = useRef(null);
+
+ const isDoctor = location.pathname.startsWith("/doctor");
+ const userId = getUserId();
+ const extensions = ["png", "jpg", "jpeg", "gif"];
+
+ // --- EFEITOS ---
+
+ // 1. Dark Mode
+ useEffect(() => {
+ const saved = localStorage.getItem(LS_KEYS.dark) === "true";
+ setDarkMode(saved);
+ document.body.classList.toggle("dark-mode", saved);
+ }, []);
+
+ // 2. Fechar ao clicar fora
+ useEffect(() => {
+ function handleClickOutside(e) {
+ if (notifRef.current && !notifRef.current.contains(e.target)) setOpenNotif(false);
+ if (profileRef.current && !profileRef.current.contains(e.target)) setOpenProfile(false);
+ }
+ document.addEventListener("click", handleClickOutside);
+ return () => document.removeEventListener("click", handleClickOutside);
+ }, []);
+
+ // 3. Carregar Avatar
+ useEffect(() => {
+ const loadAvatar = async () => {
+ if (!userId) return;
+
+ var requestOptions = {
+ headers: myHeaders,
+ method: 'GET',
+ redirect: 'follow'
+ };
+
+ const possibleNames = ['avatar', 'secretario', 'profile', 'user'];
+ for (const name of possibleNames) {
+ for (const ext of extensions) {
+ try {
+ const response = await fetch(`${supabaseUrl}/storage/v1/object/avatars/${userId}/${name}.${ext}`, requestOptions);
+ if (response.ok) {
+ const blob = await response.blob();
+ const imageUrl = URL.createObjectURL(blob);
+ setPreviewUrl(imageUrl);
+ return;
+ }
+ } catch (error) { }
+ }
+ }
+ };
+ loadAvatar();
+ }, [userId]);
+
+ // 4. Determinar Nome Amigável da Role (Função auxiliar restaurada)
+ const getFriendlyRole = () => {
+ const role = getUserRole(); // Tenta pegar do localStorage/utils primeiro
+
+ if (role) {
+ switch (role) {
+ case "medico": return "Médico";
+ case "paciente": return "Paciente";
+ case "admin": return "Administrador";
+ case "secretaria": return "Secretária";
+ default: return role;
+ }
+ }
+
+ // Fallback baseado na URL se não tiver role salva
+ if (location.pathname.startsWith("/doctor")) return "Médico";
+ if (location.pathname.startsWith("/patientapp")) return "Paciente";
+ if (location.pathname.startsWith("/admin")) return "Administrador";
+ if (location.pathname.startsWith("/secretaria")) return "Secretária";
+
+ return "Usuário";
+ };
+
+ // 5. Buscar dados do utilizador e combinar com a Role Amigável
+ useEffect(() => {
+ const fetchUserDetails = async () => {
+ // Define a role inicial baseada na lógica local (mais confiável para exibição)
+ const friendlyRole = getFriendlyRole();
+
+ if (!userId) {
+ setUserData(prev => ({ ...prev, role: friendlyRole }));
+ return;
+ }
+
+ try {
+ const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/user", {
+ method: 'GET',
+ headers: {
+ 'apikey': supabaseAK,
+ 'Authorization': `Bearer ${getAccessToken()}`
+ }
+ });
+
+ if (response.ok) {
+ const data = await response.json();
+
+ const date = data.last_sign_in_at
+ ? new Date(data.last_sign_in_at).toLocaleString('pt-PT', {
+ day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit'
+ })
+ : "Primeiro acesso";
+
+ // Se a API retornar metadados com role, usamos, senão usamos a friendlyRole calculada
+ const apiRole = data.user_metadata?.role || friendlyRole;
+
+ setUserData({
+ email: data.email,
+ role: apiRole, // Aqui agora usamos a role tratada
+ lastSignIn: date
+ });
+ } else {
+ // Se a API falhar, garante que a role local seja mostrada
+ setUserData(prev => ({ ...prev, role: friendlyRole }));
+ }
+ } catch (error) {
+ console.error("Erro ao buscar detalhes do utilizador", error);
+ setUserData(prev => ({ ...prev, role: friendlyRole }));
+ }
+ };
+
+ fetchUserDetails();
+ }, [userId, location.pathname]); // Atualiza se mudar de rota também
+
+ // --- FUNÇÕES ---
+
+ const toggleDarkMode = () => {
+ const next = !darkMode;
+ setDarkMode(next);
+ localStorage.setItem(LS_KEYS.dark, String(next));
+ document.body.classList.toggle("dark-mode", next);
+ };
+
+ const handleLogout = async () => {
+ Swal.fire({
+ title: "Tem a certeza que deseja sair?",
+ text: "Precisará de fazer login novamente para aceder ao sistema.",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#e63946",
+ cancelButtonColor: "#6c757d",
+ confirmButtonText: "Sair",
+ cancelButtonText: "Cancelar",
+ }).then(async (result) => {
+ if (result.isConfirmed) {
+ const success = await logoutUser();
+ if (success) {
+ clearUserInfo();
+ navigate("/login");
+ }
+ }
+ });
+ };
+
+ const handleAvatarUpload = () => {
+ setOpenProfile(false);
+ Swal.fire({
+ title: 'Alterar Foto de Perfil',
+ html: `
+
+
+

+
+
+
`,
+ showCancelButton: true,
+ confirmButtonText: 'Salvar',
+ preConfirm: () => {
+ const fileInput = document.getElementById('avatar-input');
+ const file = fileInput.files[0];
+ if (!file) { Swal.showValidationMessage('Selecione uma imagem'); return false; }
+ return file;
+ },
+ didOpen: () => {
+ const fileInput = document.getElementById('avatar-input');
+ const previewImg = document.getElementById('preview-avatar');
+ fileInput.addEventListener('change', (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (e) => previewImg.src = e.target.result;
+ reader.readAsDataURL(file);
+ }
+ });
+ }
+ }).then((result) => {
+ if (result.isConfirmed && result.value) {
+ uploadToSupabase(result.value);
+ }
+ });
+ };
+
+ const uploadToSupabase = async (file) => {
+ console.log("Upload simulado:", file.name);
+ };
+
+ const handleAccessibilitySettings = () => {
+ setOpenProfile(false);
+
+ let daltonismMode = localStorage.getItem(LS_KEYS.daltonism) === "true";
+ let fontScale = parseInt(localStorage.getItem(LS_KEYS.font) || "100", 10);
+ let leituraAtiva = false;
+
+ const applyFontScale = (next) => {
+ const clamped = Math.max(80, Math.min(180, next));
+ fontScale = clamped;
+ localStorage.setItem(LS_KEYS.font, String(clamped));
+ document.documentElement.style.fontSize = `${clamped}%`;
+ const fontSpan = document.getElementById('modal-font-size');
+ if (fontSpan) fontSpan.textContent = `${clamped}%`;
+ };
+
+ const toggleDaltonismMode = () => {
+ daltonismMode = !daltonismMode;
+ localStorage.setItem(LS_KEYS.daltonism, String(daltonismMode));
+ document.body.classList.toggle("daltonism-mode", daltonismMode);
+ };
+
+ let selectionChangeListener = null;
+ const lerTextoSelecionado = () => {
+ const texto = window.getSelection().toString().trim();
+ if (!texto) return;
+ window.speechSynthesis.cancel();
+ const fala = new SpeechSynthesisUtterance(texto);
+ fala.lang = "pt-PT";
+ fala.rate = 1;
+ window.speechSynthesis.speak(fala);
+ };
+
+ const toggleLeituraAtiva = () => {
+ leituraAtiva = !leituraAtiva;
+ const btn = document.getElementById('modal-toggle-leitura');
+ if (btn) {
+ btn.textContent = leituraAtiva ? "🟢 Leitura automática ativada" : "🔊 Ativar leitura automática";
+ btn.classList.toggle("active", leituraAtiva);
+ }
+ if (selectionChangeListener) {
+ document.removeEventListener("selectionchange", selectionChangeListener);
+ selectionChangeListener = null;
+ }
+ if (leituraAtiva) {
+ selectionChangeListener = () => {
+ const texto = window.getSelection().toString().trim();
+ if (texto.length > 1) lerTextoSelecionado();
+ };
+ document.addEventListener("selectionchange", selectionChangeListener);
+ } else {
+ window.speechSynthesis.cancel();
+ }
+ };
+
+ Swal.fire({
+ title: 'Configurações de Acessibilidade',
+ html: `
+
+
+
+
+ Modo daltônico
+
+
+
+
Tamanho da fonte: ${fontScale}%
+
+
+
+
+
+
+
+
+
+
+
+ `,
+ showConfirmButton: false,
+ showCancelButton: true,
+ cancelButtonText: 'Fechar',
+ cancelButtonColor: '#6c757d',
+ width: '450px',
+ didOpen: () => {
+ const popup = Swal.getPopup();
+ if (!popup) return;
+ const daltonismToggle = popup.querySelector('#daltonism-toggle');
+ const decFontBtn = popup.querySelector('#dec-font');
+ const resetFontBtn = popup.querySelector('#reset-font');
+ const incFontBtn = popup.querySelector('#inc-font');
+ const toggleLeituraBtn = popup.querySelector('#modal-toggle-leitura');
+
+ if (daltonismToggle) daltonismToggle.addEventListener('change', toggleDaltonismMode);
+ if (decFontBtn) decFontBtn.addEventListener('click', () => applyFontScale(fontScale - 10));
+ if (resetFontBtn) resetFontBtn.addEventListener('click', () => applyFontScale(100));
+ if (incFontBtn) incFontBtn.addEventListener('click', () => applyFontScale(fontScale + 10));
+ if (toggleLeituraBtn) toggleLeituraBtn.addEventListener('click', toggleLeituraAtiva);
+ },
+ willClose: () => {
+ if (selectionChangeListener) document.removeEventListener("selectionchange", selectionChangeListener);
+ window.speechSynthesis.cancel();
+ }
+ });
+ };
+
+ return (
+
+
+
+

{" "}
+
MediConnect
+
+
+
+
{ e.preventDefault(); onMenuClick(); }}>
+
+
+
+
+ -
+
+
+
+ -
+
setOpenProfile(!openProfile)} style={{ cursor: "pointer" }}>
+

+
+
+
+
+ {/* --- CORREÇÃO MODO ESCURO ---
+ Adicionei estilos condicionais baseados no estado `darkMode`.
+ Fundo: #f8f9fa (claro) / #2c2c2c (escuro)
+ Texto: #333 (claro) / #e0e0e0 (escuro)
+ Bordas: #eaeaea (claro) / #444 (escuro)
+ */}
+
+
+ {/* --- CORREÇÃO ROLE DO USUÁRIO --- */}
+ {userData.role}
+
+
+ {userData.email}
+
+
+ Login: {userData.lastSignIn}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default Navbar;
\ No newline at end of file
diff --git a/src/components/layouts/Sidebar.jsx b/src/components/layouts/Sidebar.jsx
new file mode 100644
index 0000000..826ff0a
--- /dev/null
+++ b/src/components/layouts/Sidebar.jsx
@@ -0,0 +1,120 @@
+import { getUserRole } from "../../utils/userInfo";
+import { Link, useLocation } from "react-router-dom";
+import { useState } from "react";
+import AccessibilityWidget from "../AccessibilityWidget.jsx";
+import Chatbox from "../chat/Chatbox.jsx";
+import Navbar from "./Navbar.jsx";
+function Sidebar() {
+ const [isSidebarOpen, setSidebarOpen] = useState(false);
+ const location = useLocation();
+ const role = getUserRole();
+
+ // 2. Adicione a função para alternar o estado
+ const toggleSidebar = () => {
+ setSidebarOpen(!isSidebarOpen);
+ };
+
+ // 3. Crie a string de classe que será aplicada dinamicamente
+ const mainWrapperClass = isSidebarOpen ? 'main-wrapper sidebar-open' : 'main-wrapper';
+
+ // Função para verificar se a rota está ativa
+ const isActive = (path) => {
+ const currentPath = location.pathname;
+
+ // Verificação exata primeiro
+ if (currentPath === path) return true;
+
+ // Verificação de subrotas (ex: /admin/doctorlist/edit/123)
+ if (currentPath.startsWith(path + '/')) return true;
+
+ // Verificações específicas para páginas de edição/criação
+ if (path === `/${role}/doctorlist` && (
+ currentPath.includes(`/${role}/editdoctor/`) ||
+ currentPath.includes(`/${role}/doctorform`)
+ )) return true;
+
+ if (path === `/${role}/patientlist` && (
+ currentPath.includes(`/${role}/editpatient/`) ||
+ currentPath.includes(`/${role}/patientform`)
+ )) return true;
+
+ if (path === `/${role}/consultalist` && (
+ currentPath.includes(`/${role}/consultaform`) ||
+ currentPath.includes(`/${role}/editconsulta/`)
+ )) return true;
+
+ if (path === `/${role}/createuser` && (
+ currentPath.includes(`/${role}/createuser/`)
+ )) return true;
+ if (path === `/${role}/doctor-exceptions` && (
+ currentPath.includes(`/${role}/doctor-exceptions/`)
+ )) return true;
+ if (path === `/${role}/agendadoctor` && (
+ currentPath.includes(`/${role}/editdoctorschedule/`) ||
+ currentPath.includes(`/${role}/agendaform`)
+ )) return true;
+ if (path === `/${role}/laudolist` && (
+ currentPath.includes(`/${role}/laudoedit/`) ||
+ currentPath.includes(`/${role}/laudo`) ||
+ currentPath.includes(`/${role}/laudolist/`)
+ )) return true;
+
+ return false;
+ };
+ const permissoes = {
+ admin: ['dashboard', 'consultalist', 'laudolist', 'patientlist', 'doctorlist', 'agendadoctor', 'createuser', 'excecao'],
+ medico: ['consultalist', 'dashboard', 'patientlist', 'prontuariolist', 'laudolist', 'excecao', 'agendadoctor', 'doctorcalendar'],
+ secretaria: ['dashboard', 'agendadoctor', 'consultalist', 'patientlist', 'doctorlist',],
+ paciente: ['dashboard', 'medicosdisponiveis', 'consultalist', 'laudolist', 'agendarconsulta'],
+
+ };
+ function temPermissao(role, acao) {
+ return permissoes[role]?.includes(acao);
+ }
+ const menuItems = [
+ { key: 'dashboard', label: 'Dashboard', icon: 'fa-bar-chart-o', path: 'dashboard' },
+ { key: 'doctorlist', label: 'Médicos', icon: 'fa-user-md', path: 'doctorlist' },
+ { key: 'patientlist', label: 'Pacientes', icon: 'fa-wheelchair', path: 'patientlist' },
+ { key: 'calendar', label: 'Calendario', icon: 'fa-calendar', path: 'calendar' },
+ { key: 'agendadoctor', label: 'Agenda Médica', icon: 'fa-clock-o', path: 'agendadoctor' },
+ { key: 'consultalist', label: 'Consultas', icon: 'fa-stethoscope', path: 'consultalist' },
+ { key: 'laudolist', label: 'Laudos', icon: 'fa-file-text-o', path: 'laudolist' },
+ { key: 'createuser', label: 'Usuários', icon: 'fa-users', path: 'createuser' },
+ { key: 'excecao', label: 'Exceções do Médico', icon: 'fa-calendar-times-o', path: 'excecao' },
+ { key: 'medicosdisponiveis', label: 'Agendar Consultas', icon: 'fa fa-calendar-plus-o', path: 'medicosdisponiveis' },
+ { key: 'doctorcalendar', label: 'Calendário', icon: 'fa fa-calendar', path: 'doctorcalendar' },
+ ];
+ return (
+
+
+ );
+}
+export default Sidebar;
\ No newline at end of file
diff --git a/src/components/lists/AgendaDoctor.jsx b/src/components/lists/AgendaDoctor.jsx
new file mode 100644
index 0000000..383ca72
--- /dev/null
+++ b/src/components/lists/AgendaDoctor.jsx
@@ -0,0 +1,665 @@
+import "../../assets/css/index.css"
+import { Link } from "react-router-dom";
+import { useState, useEffect, useRef, useLayoutEffect } from "react";
+import { createPortal } from "react-dom";
+import { getAccessToken } from "../../utils/auth.js";
+import { getUserRole } from "../../utils/userInfo.js";
+import { getDoctorId } from "../../utils/userInfo.js";
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+// Componente para o dropdown portal
+function DropdownPortal({ anchorEl, isOpen, onClose, className, children }) {
+ const menuRef = useRef(null);
+ const [stylePos, setStylePos] = useState({
+ position: "absolute",
+ top: 0,
+ left: 0,
+ visibility: "hidden",
+ zIndex: 1000,
+ });
+
+ useLayoutEffect(() => {
+ if (!isOpen || !anchorEl || !menuRef.current) return;
+
+ const anchorRect = anchorEl.getBoundingClientRect();
+ const menuRect = menuRef.current.getBoundingClientRect();
+ const scrollY = window.scrollY || window.pageYOffset;
+ const scrollX = window.scrollX || window.pageXOffset;
+
+ let left = anchorRect.right + scrollX - menuRect.width;
+ let top = anchorRect.bottom + scrollY;
+
+ if (left < 0) left = scrollX + 4;
+ if (top + menuRect.height > window.innerHeight + scrollY) {
+ top = anchorRect.top + scrollY - menuRect.height;
+ }
+
+ setStylePos({
+ position: "absolute",
+ top: `${Math.round(top)}px`,
+ left: `${Math.round(left)}px`,
+ visibility: "visible",
+ zIndex: 1000,
+ });
+ }, [isOpen, anchorEl, children]);
+
+ useEffect(() => {
+ if (!isOpen) return;
+
+ function handleDocClick(e) {
+ if (
+ menuRef.current &&
+ !menuRef.current.contains(e.target) &&
+ anchorEl &&
+ !anchorEl.contains(e.target)
+ ) {
+ onClose();
+ }
+ }
+
+ function handleScroll() {
+ onClose();
+ }
+
+ document.addEventListener("mousedown", handleDocClick);
+ document.addEventListener("scroll", handleScroll, true);
+
+ return () => {
+ document.removeEventListener("mousedown", handleDocClick);
+ document.removeEventListener("scroll", handleScroll, true);
+ };
+ }, [isOpen, onClose, anchorEl]);
+
+ if (!isOpen) return null;
+
+ return createPortal(
+
e.stopPropagation()}
+ >
+ {children}
+
,
+ document.body
+ );
+}
+
+function AgendaDoctor() {
+ const [agenda, setAgenda] = useState([]);
+ const [medicos, setMedicos] = useState([]);
+ const [openDropdown, setOpenDropdown] = useState(null);
+ const [search, setSearch] = useState("");
+ const [dayFilter, setDayFilter] = useState("");
+ const [typeFilter, setTypeFilter] = useState("");
+ const [deleteId, setDeleteId] = useState(null);
+ const [editId, setEditId] = useState(null);
+ const [editData, setEditData] = useState({
+ doctor_id: "",
+ weekday: "",
+ start_time: "",
+ end_time: "",
+ slot_minutes: 30,
+ appointment_type: "",
+ active: true,
+ });
+ const anchorRefs = useRef({});
+ const role = getUserRole();
+ const tokenUsuario = getAccessToken();
+
+ const requestOptions = {
+ method: "GET",
+ headers: {
+ apikey:
+ supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ redirect: "follow",
+ };
+
+ // Fetch agenda
+ useEffect(() => {
+ if (getUserRole() === 'medico') {
+ fetch(
+ `${supabaseUrl}/rest/v1/doctor_availability?doctor_id=eq.${getDoctorId()}`,
+ requestOptions
+ )
+ .then((res) => res.json())
+ .then((result) => setAgenda(Array.isArray(result) ? result : []))
+ .catch((err) => console.log(err));
+ } else {
+ fetch(
+ `${supabaseUrl}/rest/v1/doctor_availability`,
+ requestOptions
+ )
+ .then((res) => res.json())
+ .then((result) => setAgenda(Array.isArray(result) ? result : []))
+ .catch((err) => console.log(err));
+ }
+ }, []);
+
+ // Fetch médicos
+ useEffect(() => {
+ fetch("https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors", requestOptions)
+ .then((res) => res.json())
+ .then((result) => setMedicos(Array.isArray(result) ? result : []))
+ .catch((err) => console.log(err));
+ }, []);
+
+ const getDoctorName = (id) => {
+ if (!id) return "";
+ const medico = medicos.find((m) => m.id === id);
+ return medico ? medico.full_name || medico.name || "" : id;
+ };
+
+ // DELETE
+ const handleDelete = (id) => setDeleteId(id);
+
+ const confirmDelete = () => {
+ if (!deleteId) return;
+
+ fetch(
+ `https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability?id=eq.${deleteId}`,
+ {
+ method: "DELETE",
+ headers: {
+ apikey:
+ supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ }
+ )
+ .then((res) => {
+ if (!res.ok) throw new Error("Erro ao deletar a agenda");
+ setAgenda((prev) => prev.filter((a) => a.id !== deleteId));
+ setDeleteId(null);
+ })
+ .catch((err) => console.log(err));
+ };
+
+ // EDIT
+ const handleEditClick = (id) => {
+ const agendaItem = agenda.find((a) => a.id === id);
+ if (!agendaItem) return;
+
+ setEditData({
+ doctor_id: agendaItem.doctor_id || "",
+ weekday: agendaItem.weekday || "",
+ start_time: agendaItem.start_time || "",
+ end_time: agendaItem.end_time || "",
+ slot_minutes: agendaItem.slot_minutes || 30,
+ appointment_type: agendaItem.appointment_type || "",
+ active: agendaItem.active ?? true,
+ });
+ setEditId(id);
+ setOpenDropdown(null);
+ };
+
+ const handleEditChange = (e) => {
+ const { name, value, type, checked } = e.target;
+ setEditData((prev) => ({
+ ...prev,
+ [name]: type === "checkbox" ? checked : value,
+ }));
+ };
+
+ const submitEdit = () => {
+ if (!editId) return;
+
+ if (!editData.doctor_id) {
+ alert("Selecione um médico válido.");
+ return;
+ }
+ if (!editData.weekday || !editData.start_time || !editData.end_time || !editData.appointment_type) {
+ alert("Preencha todos os campos obrigatórios.");
+ return;
+ }
+
+ fetch(
+ `https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctor_availability?id=eq.${editId}`,
+ {
+ method: "PATCH",
+ headers: {
+ apikey:
+ supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ "Content-Type": "application/json",
+ "Prefer": "return=representation", // ESSENCIAL
+ },
+ body: JSON.stringify(editData),
+ }
+ )
+ .then((res) => {
+ if (!res.ok) throw new Error("Erro ao salvar alterações");
+ return res.json();
+ })
+ .then((updated) => {
+ setAgenda((prev) =>
+ prev.map((a) => (a.id === editId ? { ...a, ...updated[0] } : a))
+ );
+ setEditId(null);
+ })
+ .catch((err) => {
+ console.error(err);
+ alert("Erro ao salvar alterações. Verifique os campos e tente novamente.");
+ });
+ };
+
+ const filteredAgenda = agenda.filter((a) => {
+ if (!a) return false;
+ const q = search.toLowerCase();
+
+ // Filtro por texto (nome do médico, dia, tipo)
+ const matchesText = (
+ (getDoctorName(a.doctor_id) || "").toLowerCase().includes(q) ||
+ (a.weekday || "").toLowerCase().includes(q) ||
+ (a.appointment_type || "").toLowerCase().includes(q)
+ );
+
+ // Filtro por dia da semana
+ const matchesDay = !dayFilter || a.weekday === dayFilter;
+
+ // Filtro por tipo de consulta
+ const matchesType = !typeFilter || a.appointment_type === typeFilter;
+
+ return matchesText && matchesDay && matchesType;
+ });
+
+ // Paginação
+ const [itemsPerPage1, setItemsPerPage1] = useState(15);
+ const [currentPage1, setCurrentPage1] = useState(1);
+ const indexOfLastAgenda = currentPage1 * itemsPerPage1;
+ const indexOfFirstAgenda = indexOfLastAgenda - itemsPerPage1;
+ const currentAgenda = filteredAgenda.slice(indexOfFirstAgenda, indexOfLastAgenda);
+ const totalPages1 = Math.ceil(filteredAgenda.length / itemsPerPage1);
+
+ // Reset da paginação quando filtros mudam
+ useEffect(() => {
+ setCurrentPage1(1);
+ }, [search, dayFilter, typeFilter]);
+ const permissoes = {
+ admin: ['nome'],
+ secretaria: ['nome'],
+ medico: ['']
+ };
+ const pode = (acao) => permissoes[role]?.includes(acao);
+ return (
+
+
+ {/* Header com título e botão */}
+
+
Agenda Médica
+
+ Adicionar agenda
+
+
+
+ {/* Todos os filtros em uma única linha */}
+
+ {/* Campo de busca */}
+ setSearch(e.target.value)}
+ style={{ minWidth: "300px", maxWidth: "450px", }}
+ />
+
+ {/* Filtro por dia da semana */}
+ setDayFilter(e.target.value)}
+ >
+
+
+
+
+
+
+
+
+
+
+ {/* Filtro por tipo de consulta */}
+ setTypeFilter(e.target.value)}
+ >
+
+
+
+
+
+
+ {/* Tabela */}
+
+
+
+
+ {pode('nome') && | Nome | }
+ Dias disponíveis |
+ Horário disponível |
+ Duração (min) |
+ Tipo |
+ Status |
+ Ação |
+
+
+
+ {currentAgenda.length > 0 ? (
+ currentAgenda.map((a) => (
+
+ {pode('nome') && | {getDoctorName(a.doctor_id)} | }
+
+
+
+ {a.weekday === 'monday' ? 'Segunda' :
+ a.weekday === 'tuesday' ? 'Terça' :
+ a.weekday === 'wednesday' ? 'Quarta' :
+ a.weekday === 'thursday' ? 'Quinta' :
+ a.weekday === 'friday' ? 'Sexta' :
+ a.weekday === 'saturday' ? 'Sábado' :
+ a.weekday === 'sunday' ? 'Domingo' :
+ a.weekday}
+
+ |
+
+ {a.start_time || ""} ás {a.end_time || ""}
+ |
+ {a.slot_minutes || 30} |
+
+
+ {a.appointment_type === 'presencial' ? (
+ <>
+
+ Presencial
+ >
+ ) : a.appointment_type === 'telemedicina' ? (
+ <>
+
+ Telemedicina
+ >
+ ) : (
+ a.appointment_type
+ )}
+
+ |
+
+
+ {a.active ? (
+ <>
+
+ Ativo
+ >
+ ) : (
+ <>
+
+ Inativo
+ >
+ )}
+
+ |
+
+
+
+
+
+ |
+
+ ))
+ ) : (
+
+ |
+ Nenhuma agenda encontrada
+ |
+
+ )}
+
+
+
+
+ {/* Paginação */}
+
+
+ Total encontrados: {filteredAgenda.length}
+
+
+ {
+ setItemsPerPage1(Number(e.target.value));
+ setCurrentPage1(1);
+ }}
+ title="Itens por página"
+ >
+
+
+
+
+
+
+
+
+
+
+
+ {/* Modal de Delete */}
+ {deleteId && (
+
+
+
+
+

+
Tem certeza que deseja deletar esta agenda?
+
+
+
+
+
+
+
+
+ )}
+
+ {/* Modal de Edit */}
+ {editId && (
+
+
+
+
+
Editar Disponibilidade
+
+
+
+
+
+
+
+ {medicos.map((m) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+ }
+export default AgendaDoctor;
diff --git a/src/components/lists/ConsultaList.jsx b/src/components/lists/ConsultaList.jsx
new file mode 100644
index 0000000..ce351c9
--- /dev/null
+++ b/src/components/lists/ConsultaList.jsx
@@ -0,0 +1,813 @@
+import "../../assets/css/index.css"
+import { Link } from "react-router-dom";
+import { useState, useEffect, useRef, useLayoutEffect } from "react";
+import { createPortal } from "react-dom";
+import { getAccessToken} from "../../utils/auth.js";
+import Swal from "sweetalert2";
+import { useResponsive } from '../../utils/useResponsive.js';
+import { useNavigate } from "react-router-dom";
+import { getUserRole } from "../../utils/userInfo.js";
+
+
+function DropdownPortal({ anchorEl, isOpen, onClose, className, children }) {
+ const menuRef = useRef(null);
+ const [stylePos, setStylePos] = useState({
+ position: "absolute",
+ top: 0,
+ left: 0,
+ visibility: "hidden",
+ zIndex: 1000,
+ });
+
+ // Posiciona o menu após renderar (medir tamanho do menu)
+ useLayoutEffect(() => {
+ if (!isOpen) return;
+ if (!anchorEl || !menuRef.current) return;
+
+ const anchorRect = anchorEl.getBoundingClientRect();
+ const menuRect = menuRef.current.getBoundingClientRect();
+ const scrollY = window.scrollY || window.pageYOffset;
+ const scrollX = window.scrollX || window.pageXOffset;
+
+ // tenta alinhar à direita do botão (como dropdown-menu-right)
+ let left = anchorRect.right + scrollX - menuRect.width;
+ let top = anchorRect.bottom + scrollY;
+
+ // evita sair da esquerda da tela
+ if (left < 0) left = scrollX + 4;
+ // se extrapolar bottom, abre para cima
+ if (top + menuRect.height > window.innerHeight + scrollY) {
+ top = anchorRect.top + scrollY - menuRect.height;
+ }
+ setStylePos({
+ position: "absolute",
+ top: `${Math.round(top)}px`,
+ left: `${Math.round(left)}px`,
+ visibility: "visible",
+ zIndex: 1000,
+ });
+ }, [isOpen, anchorEl, children]);
+
+ // fecha ao clicar fora / ao rolar
+ useEffect(() => {
+ if (!isOpen) return;
+ function handleDocClick(e) {
+ const menu = menuRef.current;
+ if (menu && !menu.contains(e.target) && anchorEl && !anchorEl.contains(e.target)) {
+ onClose();
+ }
+ }
+ function handleScroll() {
+ onClose();
+ }
+ document.addEventListener("mousedown", handleDocClick);
+ // captura scroll em qualquer elemento (true)
+ document.addEventListener("scroll", handleScroll, true);
+ return () => {
+ document.removeEventListener("mousedown", handleDocClick);
+ document.removeEventListener("scroll", handleScroll, true);
+ };
+ }, [isOpen, onClose, anchorEl]);
+
+ if (!isOpen) return null;
+ return createPortal(
+
e.stopPropagation()}
+ >
+ {children}
+
,
+ document.body
+ );
+}
+
+function ConsultaList() {
+ const [openDropdown, setOpenDropdown] = useState(null);
+ const anchorRefs = useRef({});
+ const [consulta, setConsultas] = useState([]);
+ const [search, setSearch] = useState("");
+ const [statusFilter, setStatusFilter] = useState("");
+ const [typeFilter, setTypeFilter] = useState("");
+ const tokenUsuario = getAccessToken()
+ const [pacientesMap, setPacientesMap] = useState({});
+ const [medicosMap, setMedicosMap] = useState({});
+ const [period, setPeriod] = useState("");
+ const [startDate, setStartDate] = useState("");
+ const [endDate, setEndDate] = useState("");
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const headers = {
+ apikey: supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ "Content-Type": "application/json",
+ };
+
+ var myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+ var requestOptions = {
+ method: 'GET',
+ headers: myHeaders,
+ redirect: 'follow'
+ };
+ useEffect(() => {
+ fetch(`${supabaseUrl}/rest/v1/appointments`, requestOptions)
+ .then(response => response.json())
+ .then(result => setConsultas(Array.isArray(result) ? result : []))
+ .catch(error => console.log('error', error));
+ }, [])
+
+ const handleDelete = async (id) => {
+ if (getUserRole() === 'paciente') {
+ Swal.fire("Ação não permitida", "Pacientes não podem excluir consultas. Por favor, entre em contato com a secretaria.", "warning");
+ return;
+ }
+ const confirm = await Swal.fire({
+ title: "Tem certeza?",
+ text: "Deseja realmente excluir esta consulta? Essa ação não poderá ser desfeita.",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#e63946",
+ cancelButtonColor: "#6c757d",
+ confirmButtonText: "Excluir!",
+ cancelButtonText: "Cancelar",
+ });
+
+ if (!confirm.isConfirmed) return;
+
+ try {
+ const response = await fetch(
+ `${supabaseUrl}/rest/v1/appointments?id=eq.${id}`,
+ {
+ method: "DELETE",
+ headers: myHeaders,
+ }
+ );
+ console.log("Resposta do delete:", response);
+ if (response.ok) {
+ setConsultas((prev) => prev.filter((c) => c.id !== id));
+ setOpenDropdown(null);
+
+ Swal.fire({
+ title: "Excluída!",
+ text: "A consulta foi removida com sucesso.",
+ icon: "success",
+ timer: 2000,
+ showConfirmButton: false,
+ });
+ } else {
+ Swal.fire("Erro", "Falha ao excluir a consulta. Tente novamente.", "error");
+ }
+ } catch (error) {
+ console.error("Erro ao deletar:", error);
+ Swal.fire("Erro", "Não foi possível conectar ao servidor.", "error");
+ }
+ };
+
+ const filteredConsultas = consulta.filter(p => {
+ if (!p) return false;
+ const nome = (pacientesMap[p.patient_id] || "").toLowerCase();
+ const médicoNome = (medicosMap[p.doctor_id] || "").toLowerCase();
+ const cpf = (p.cpf || "").toLowerCase();
+ const email = (p.email || "").toLowerCase();
+ const q = search.toLowerCase();
+
+
+ // Filtro por texto (nome, cpf, email)
+ const matchesText = nome.includes(q) || cpf.includes(q) || email.includes(q) || médicoNome.includes(q);
+
+ // Filtro por status
+ const matchesStatus = !statusFilter || p.status === statusFilter;
+
+ // Filtro por tipo de consulta
+ const matchesType = !typeFilter || p.appointment_type === typeFilter;
+
+
+ let dateMatch = true;
+ if (p.scheduled_at) {
+ const consultaDate = new Date(p.scheduled_at);
+ const today = new Date();
+
+ // Filtros por período rápido
+ if (period === "today") {
+ const todayStr = today.toDateString();
+ dateMatch = consultaDate.toDateString() === todayStr;
+ } else if (period === "week") {
+ const startOfWeek = new Date(today);
+ startOfWeek.setDate(today.getDate() - today.getDay());
+ startOfWeek.setHours(0, 0, 0, 0);
+ const endOfWeek = new Date(startOfWeek);
+ endOfWeek.setDate(startOfWeek.getDate() + 6);
+ endOfWeek.setHours(23, 59, 59, 999);
+ dateMatch = consultaDate >= startOfWeek && consultaDate <= endOfWeek;
+ } else if (period === "month") {
+ dateMatch = consultaDate.getMonth() === today.getMonth() &&
+ consultaDate.getFullYear() === today.getFullYear();
+ }
+
+ // Filtros por data específica
+ if (startDate && endDate) {
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+ end.setHours(23, 59, 59, 999); // Inclui o dia inteiro
+ dateMatch = dateMatch && consultaDate >= start && consultaDate <= end;
+ } else if (startDate) {
+ const start = new Date(startDate);
+ dateMatch = dateMatch && consultaDate >= start;
+ } else if (endDate) {
+ const end = new Date(endDate);
+ end.setHours(23, 59, 59, 999);
+ dateMatch = dateMatch && consultaDate <= end;
+ }
+ }
+
+ return matchesText && matchesStatus && matchesType && dateMatch;
+ }).sort((a, b) => {
+ // Priorizar consultas "requested" (solicitadas) primeiro
+ if (a.status === 'requested' && b.status !== 'requested') {
+ return -1; // 'a' vem antes de 'b'
+ }
+ if (b.status === 'requested' && a.status !== 'requested') {
+ return 1; // 'b' vem antes de 'a'
+ }
+
+ // Se ambos têm o mesmo status de prioridade, ordena por data (mais recente primeiro)
+ const dateA = new Date(a.scheduled_at || 0);
+ const dateB = new Date(b.scheduled_at || 0);
+ return dateB - dateA;
+ });
+ const [itemsPerPage1, setItemsPerPage1] = useState(15);
+ const [currentPage1, setCurrentPage1] = useState(1);
+ const indexOfLastPatient = currentPage1 * itemsPerPage1;
+ const indexOfFirstPatient = indexOfLastPatient - itemsPerPage1;
+ const currentConsultas = filteredConsultas.slice(indexOfFirstPatient, indexOfLastPatient);
+ const totalPages1 = Math.ceil(filteredConsultas.length / itemsPerPage1);
+ useEffect(() => {
+ setCurrentPage1(1);
+ }, [search, statusFilter, typeFilter, period, startDate, endDate]);
+
+ // Função para definir períodos e limpar datas
+ const handlePeriodChange = (newPeriod) => {
+ // Se clicar no mesmo período, limpa o filtro
+ if (period === newPeriod) {
+ setPeriod("");
+ } else {
+ setPeriod(newPeriod);
+ }
+
+ // Sempre limpa as datas específicas
+ setStartDate("");
+ setEndDate("");
+ };
+useEffect(() => {
+ if (!consulta || consulta.length === 0) return;
+
+ const buscarPacientes = async () => {
+ try {
+ // Pega IDs únicos de pacientes
+ const idsUnicos = [...new Set(consulta.map((c) => c.patient_id))];
+
+ // Faz apenas 1 fetch por paciente
+ const promises = idsUnicos.map(async (id) => {
+ try {
+ const res = await fetch(
+ `${supabaseUrl}/rest/v1/patients?id=eq.${id}`,
+ {
+ method: "GET",
+ headers: {
+ apikey:
+ supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ }
+ );
+ const data = await res.json();
+ return { id, full_name: data[0]?.full_name || "Nome não encontrado" };
+ } catch (err) {
+ return { id, full_name: "Nome não encontrado" };
+ }
+ });
+
+ const results = await Promise.all(promises);
+
+ const map = {};
+ results.forEach((r) => (map[r.id] = r.full_name));
+ setPacientesMap(map);
+ } catch (err) {
+ console.error("Erro ao buscar pacientes:", err);
+ }
+ };
+
+ buscarPacientes();
+ }, [consulta]);
+ useEffect(() => {
+ if (!Array.isArray(consulta) || consulta.length === 0) return;
+
+ const buscarMedicos = async () => {
+ try {
+ const idsUnicos = [...new Set(consulta.map((c) => c.doctor_id).filter(Boolean))];
+ if (idsUnicos.length === 0) return;
+
+ const headers = {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${tokenUsuario}`,
+ apikey: supabaseAK,
+ };
+
+ const promises = idsUnicos.map(async (id) => {
+ try {
+ const res = await fetch(`${supabaseUrl}/rest/v1/doctors?id=eq.${id}`, {
+ method: "GET",
+ headers,
+ });
+ if (!res.ok) return { id, full_name: "Nome não encontrado" };
+ const data = await res.json();
+ return { id, full_name: data?.[0]?.full_name || "Nome não encontrado" };
+ } catch {
+ return { id, full_name: "Nome não encontrado" };
+ }
+ });
+
+ const results = await Promise.all(promises);
+ const map = {};
+ results.forEach((r) => (map[r.id] = r.full_name));
+ setMedicosMap(map);
+ } catch (err) {
+ console.error("Erro ao buscar nomes dos médicos:", err);
+ }
+ };
+
+ buscarMedicos();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [consulta]);
+ const formatDate = (dateString) => {
+ if (!dateString) return 'N/A';
+ try {
+ // Extrai data e hora diretamente da string ISO sem conversão de timezone
+ const [datePart, timePart] = dateString.split('T');
+ const [year, month, day] = datePart.split('-');
+ const [hour, minute] = timePart.split(':');
+
+ return `${day}/${month}/${year} ${hour}:${minute}`;
+ } catch {
+ return dateString;
+ }
+ };
+ const handleConfirm = async (id) => {
+ if (getUserRole() === 'paciente') {
+ Swal.fire("Ação não permitida", "Pacientes não podem confirmar consultas diretamente. Por favor, entre em contato com a secretaria.", "warning");
+ return;
+ }
+ const confirm = await Swal.fire({
+ title: "Confirmar consulta?",
+ text: "Esta ação irá confirmar a consulta.",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#4caf50",
+ cancelButtonColor: "#6c757d",
+ confirmButtonText: "Confirmar consulta",
+ cancelButtonText: "Voltar",
+ });
+
+ if (!confirm.isConfirmed) return;
+
+ try {
+ const response = await fetch(
+ `${supabaseUrl}/rest/v1/appointments?id=eq.${id}`,
+ {
+ method: "PATCH",
+ headers: {
+ ...headers,
+ "Prefer": "return=minimal"
+ },
+ body: JSON.stringify({ status: "confirmed" })
+ }
+ );
+
+ if (response.ok) {
+ // Atualiza o estado local
+ setConsultas((prev) =>
+ prev.map((c) =>
+ c.id === id ? { ...c, status: "confirmed" } : c
+ )
+ );
+
+ Swal.fire({
+ title: "Confirmado!",
+ text: "Consulta confirmada com sucesso.",
+ icon: "success",
+ timer: 2000,
+ showConfirmButton: false,
+ });
+ } else {
+ throw new Error('Falha na confirmação');
+ }
+ } catch (error) {
+ console.error("Erro ao confirmar:", error);
+ Swal.fire("Erro", "Não foi possível confirmar a consulta.", "error");
+ }
+};
+
+const handleCancel = async (id) => {
+ if (getUserRole() === 'paciente') {
+ Swal.fire("Ação não permitida", "Pacientes não podem cancelar consultas diretamente. Por favor, entre em contato com a secretaria.", "warning");
+ return;
+ }
+ const confirm = await Swal.fire({
+ title: "Cancelar consulta?",
+ text: "Esta ação irá cancelar a consulta.",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#e63946",
+ cancelButtonColor: "#6c757d",
+ confirmButtonText: "Cancelar consulta",
+ cancelButtonText: "Voltar",
+ });
+
+ if (!confirm.isConfirmed) return;
+
+ try {
+ const response = await fetch(
+ `${supabaseUrl}/rest/v1/appointments?id=eq.${id}`,
+ {
+ method: "PATCH",
+ headers: {
+ ...headers,
+ },
+ body: JSON.stringify({ status: "cancelled" })
+ }
+ );
+
+ if (response.ok) {
+ setConsultas((prev) =>
+ prev.map((c) =>
+ c.id === id ? { ...c, status: "cancelled" } : c
+ )
+ );
+
+ Swal.fire({
+ title: "Cancelado!",
+ text: "Consulta cancelada com sucesso.",
+ icon: "success",
+ timer: 2000,
+ showConfirmButton: false,
+ });
+ } else {
+ throw new Error('Falha no cancelamento');
+ }
+ } catch (error) {
+ console.error("Erro ao cancelar:", error);
+ Swal.fire("Erro", "Não foi possível cancelar a consulta.", "error");
+ }
+};
+
+ const navigate = useNavigate();
+ const role = getUserRole();
+ const permissoes = {
+ admin: ['editconsulta', 'deletarconsulta', 'consultaform', 'viewactionconsultas' , 'nomepaciente'],
+ medico: ['editconsulta', 'deletarconsulta', 'consultaform', 'viewactionconsultas', 'nomepaciente'],
+ secretaria: ['editconsulta', 'deletarconsulta', 'consultaform', 'viewactionconsultas', 'nomepaciente'],
+ paciente: ['']
+};
+ const pode = (acao) => permissoes[role]?.includes(acao);
+ function hasAnyAction(c) {
+ return (
+ pode('editconsulta') ||
+ pode('deletarconsulta') ||
+ (c.status === 'confirmed' && pode('viewactionconsultas')) ||
+ (c.appointment_type === 'telemedicina' && c.status === 'confirmed') ||
+ (c.status === 'requested' && pode('viewactionconsultas'))
+ );
+}
+ return (
+
+
+
+
+
+ {pode('consultaform') && (
+
+
Lista de consultas
+
+ Adicionar consulta
+
+
+ )}
+
+ {/* Todos os filtros em uma única linha */}
+
+ {/* Campo de busca */}
+
setSearch(e.target.value)}
+ />
+
+ {/* Filtro de status */}
+
setStatusFilter(e.target.value)}
+ >
+
+
+
+
+
+
+
+ {/* Filtro De */}
+
+
+ {
+ setStartDate(e.target.value);
+ if (e.target.value) setPeriod("");
+ }}
+ />
+
+
+ {/* Filtro Até */}
+
+
+ {
+ setEndDate(e.target.value);
+ if (e.target.value) setPeriod("");
+ }}
+ />
+
+
+ {/* Botões rápidos */}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Pedido |
+ {pode('nomepaciente') && Nome do Paciente | }
+ Nome do Médico |
+ Agendado |
+ Duração |
+ Modo |
+ Status |
+ {currentConsultas.some(hasAnyAction) && Ação | }
+
+
+
+ {currentConsultas.length > 0 ? (
+ currentConsultas.map((c) => (
+
+ | {c.order_number} |
+ {pode('nomepaciente') && {pacientesMap[c.patient_id] || "Carregando..."} | }
+ {medicosMap[c.doctor_id] || "Carregando..."} |
+ {formatDate(c.scheduled_at)} |
+ {c.duration_minutes} min |
+
+
+ {c.appointment_type === 'presencial' ? (
+ <>
+
+ Presencial
+ >
+ ) : c.appointment_type === 'telemedicina' ? (
+ <>
+
+ Telemedicina
+ >
+ ) : (
+ c.appointment_type
+ )}
+
+ |
+
+
+
+ {c.status === 'requested' ? (
+ <>
+
+ Solicitado
+ >
+ ) : c.status === 'confirmed' ? (
+ <>
+
+ Confirmado
+ >
+ ) : c.status === 'completed' ? (
+ <>
+
+ Concluído
+ >
+ ) : c.status === 'cancelled' ? (
+ <>
+
+ Cancelado
+ >
+ ) : (
+ <>
+
+ {c.status}
+ >
+ )}
+
+ |
+ {currentConsultas.some(hasAnyAction) && (
+ hasAnyAction(c) ? (
+
+
+ {c.appointment_type !== 'telemedicina' && c.status === 'confirmed' && (
+
+ )}
+ {c.appointment_type === 'telemedicina' && c.status === 'confirmed' && (
+
+ )}
+ {pode('editconsulta') && (
+
+ )}
+
+ {c.status === 'requested' && pode('viewactionconsultas') && (
+ <>
+
+
+ >
+ )}
+
+
+ |
+ ) : (
+ - |
+ )
+ )}
+
+ ))
+ ) : (
+
+ |
+ Nenhuma consulta encontrada.
+ |
+
+ )}
+
+
+
+
+
+ Total encontrados: {filteredConsultas.length}
+
+
+ {
+ setItemsPerPage1(Number(e.target.value));
+ setCurrentPage1(1);
+ }}
+ title="Itens por página"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default ConsultaList;
\ No newline at end of file
diff --git a/src/components/lists/DoctorList.jsx b/src/components/lists/DoctorList.jsx
new file mode 100644
index 0000000..714e5f5
--- /dev/null
+++ b/src/components/lists/DoctorList.jsx
@@ -0,0 +1,405 @@
+import "../../assets/css/index.css";
+import { useState, useEffect } from "react";
+import { Link } from "react-router-dom";
+import supabase from "../../Supabase.js";
+import Swal from "sweetalert2";
+import { getAccessToken } from "../../utils/auth.js";
+import { getUserRole } from "../../utils/userInfo.js";
+
+
+const AvatarForm = "/img/AvatarForm.jpg";
+
+
+function DoctorList() {
+ const [search, setSearch] = useState("");
+ const [specialtyFilter, setSpecialtyFilter] = useState(""); // Filtro por especialidade
+ const [doctors, setDoctors] = useState([]);
+ const [openDropdown, setOpenDropdown] = useState(null);
+ const tokenUsuario = getAccessToken()
+ const role = getUserRole();
+ var myHeaders = new Headers();
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ myHeaders.append(
+ "apikey",
+ supabaseAK
+ );
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+
+ var requestOptions = {
+ method: "GET",
+ headers: myHeaders,
+ redirect: "follow",
+ };
+
+ // buscar médicos
+ useEffect(() => {
+ fetch(`${supabaseUrl}/rest/v1/doctors`, requestOptions)
+ .then((response) => response.json())
+ .then((result) => setDoctors(Array.isArray(result) ? result : []))
+ .catch((error) => console.log("error", error));
+ }, []);
+
+
+ const handleDelete = async (id) => {
+ Swal.fire({
+ title: "Tem certeza?",
+ text: "Tem certeza que deseja excluir este registro?",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#3085d6",
+ cancelButtonColor: "#d33",
+ confirmButtonText: "Sim, excluir",
+ cancelButtonText: "Cancelar"
+ }).then(async (result) => {
+ if (result.isConfirmed) {
+ try {
+ const tokenUsuario = getAccessToken(); // pega o token do usuário (mesmo que usa no form)
+
+ var myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+ myHeaders.append("Content-Type", "application/json");
+
+ const response = await fetch(
+ `${supabaseUrl}/rest/v1/doctors?id=eq.${id}`,
+ {
+ method: "DELETE",
+ headers: myHeaders,
+ }
+ );
+
+ if (!response.ok) {
+ const err = await response.json();
+ console.error("Erro ao deletar médico:", err);
+ Swal.fire("Erro!", err.message || "Não foi possível excluir o registro.", "error");
+ return;
+ }
+
+ // Atualiza a lista local
+ setDoctors((prev) => prev.filter((doc) => doc.id !== id));
+
+ Swal.fire("Excluído!", "O registro foi removido com sucesso.", "success");
+ } catch (error) {
+ console.error("Erro inesperado:", error);
+ Swal.fire("Erro!", "Algo deu errado ao excluir.", "error");
+ }
+ }
+ });
+ };
+
+ const handleViewDetails = async (id) => {
+ try {
+ const tokenUsuario = getAccessToken();
+
+ const response = await fetch(
+ `${supabaseUrl}/rest/v1/doctors?id=eq.${id}`,
+ {
+ method: "GET",
+ headers: {
+ apikey:
+ supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ }
+ );
+
+ const data = await response.json();
+ const doctor = data[0];
+
+ if (!doctor) {
+ Swal.fire("Erro", "Não foi possível carregar os detalhes do médico.", "error");
+ return;
+ }
+
+ Swal.fire({
+ width: "800px",
+ showConfirmButton: true,
+ confirmButtonText: "Fechar",
+ confirmButtonColor: "#4dabf7",
+ background: document.body.classList.contains("dark-mode") ? "#1e1e2f" : "#fff",
+ color: document.body.classList.contains("dark-mode") ? "#f5f5f5" : "#000",
+ html: `
+
+
+
+
Perfil Médico
+
+
+
+
+
+

+
${doctor.full_name}
+
${doctor.specialty || "Especialidade não informada"}
+
+
+
+
+
+
Telefone: ${doctor.phone_mobile || "—"}
+
Email: ${doctor.email || "—"}
+
Data de nascimento: ${doctor.birth_date || "—"}
+
Sexo: ${doctor.gender || "—"}
+
+
+
Região: ${doctor.city || "—"}, ${doctor.state || "—"}, Brasil
+
CRM: ${doctor.crm || "—"}
+
Especialidade: ${doctor.specialty || "—"}
+
Experiência: ${doctor.experience_years || "—"} anos
+
+
+
+
+
+
Biografia
+
+ ${doctor.biografia || "Este médico ainda não possui biografia cadastrada."}
+
+
+
+ `,
+ didOpen: () => {
+ document
+ .getElementById("btn-close-modal")
+ ?.addEventListener("click", () => Swal.close());
+ },
+ });
+ } catch (err) {
+ console.error("Erro ao buscar médico:", err);
+ Swal.fire("Erro!", err.message || "Erro ao buscar médico.", "error");
+ }
+ };
+
+ // Função de filtragem (mesmo padrão do PatientList)
+ const filteredDoctors = doctors.filter(doctor => {
+ if (!doctor) return false;
+
+ // Filtro por texto (nome, especialidade, CRM, email)
+ const nome = (doctor.full_name || "").toLowerCase();
+ const crm = (doctor.crm || "").toLowerCase();
+ const email = (doctor.email || "").toLowerCase();
+ const cidade = (doctor.city || "").toLowerCase();
+ const q = search.toLowerCase();
+ const matchesSearch = nome.includes(q) || crm.includes(q) || email.includes(q) || cidade.includes(q);
+
+ // Filtro por especialidade
+ let matchesSpecialty = true;
+ if (specialtyFilter) {
+ const doctorSpecialty = (doctor.specialty || "").toLowerCase().trim();
+ matchesSpecialty = doctorSpecialty.includes(specialtyFilter.toLowerCase());
+ }
+
+ return matchesSearch && matchesSpecialty;
+ });
+ const permissoes = {
+ admin: ['adddoctor'],
+ secretaria: [""],
+ paciente: ['']
+};
+ const pode = (acao) => permissoes[role]?.includes(acao);
+ return (
+
+
+
+
+
+
Lista de Médicos
+ {pode('adddoctor') && (
+ Adicionar Médico
+
+ )}
+
+
+ {/* Filtros em uma única linha (mesmo padrão do PatientList) */}
+
+ {/* Campo de busca */}
+ setSearch(e.target.value)}
+ style={{ minWidth: "300px", maxWidth: "450px" }}
+ />
+
+ {/* Filtro por especialidade */}
+ setSpecialtyFilter(e.target.value)}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Contador de resultados */}
+
+ {filteredDoctors.length} médico(s) encontrado(s)
+
+
+
+
+
+
+ {filteredDoctors.length > 0 ? (
+ filteredDoctors.map((doctor) => (
+
+
+
+
+

+
+
+
+ {/* Dropdown estilizado */}
+
+
+
+ {openDropdown === doctor.id && (
+
+ {/* Ver Detalhes */}
+ {
+ e.stopPropagation();
+ setOpenDropdown(null);
+ handleViewDetails(doctor.id);
+ }}
+ >
+ Ver Detalhes
+
+
+ {/* Edit */}
+
+ Editar
+
+
+ {/* Delete */}
+
+
+ )}
+
+
+
+
+ {doctor.full_name}
+
+
+
{doctor.specialty || 'Não informado'}
+
+ {doctor.city || 'Não informado'}
+
+
+
+ ))
+ ) : (
+
+
+
+
Nenhum médico encontrado
+
+ {search || specialtyFilter
+ ? "Tente ajustar os filtros de busca"
+ : "Nenhum médico cadastrado no sistema"}
+
+
+
+ )}
+
+
+
+ {/* Modal delete (não alterado) */}
+
+
+
+
+

+
Are you sure want to delete this Doctor?
+
+
+
+
+
+
+ );
+}
+
+export default DoctorList;
diff --git a/src/components/lists/LaudoList.jsx b/src/components/lists/LaudoList.jsx
new file mode 100644
index 0000000..0a2d62e
--- /dev/null
+++ b/src/components/lists/LaudoList.jsx
@@ -0,0 +1,755 @@
+import { Link } from "react-router-dom";
+import React, { useState, useRef, useLayoutEffect, useEffect } from "react";
+import { createPortal } from "react-dom";
+import { getAccessToken } from "../../utils/auth.js";
+import Swal from 'sweetalert2';
+import { useResponsive } from '../../utils/useResponsive';
+import { useNavigate } from "react-router-dom";
+import { getUserRole } from "../../utils/userInfo.js";
+
+
+function DropdownPortal({ anchorEl, isOpen, onClose, className, children }) {
+ const menuRef = useRef(null);
+ const [stylePos, setStylePos] = useState({
+ position: "absolute",
+ top: 0,
+ left: 0,
+ visibility: "hidden",
+ zIndex: 1000,
+ });
+
+ useLayoutEffect(() => {
+ if (!isOpen || !anchorEl || !menuRef.current) return;
+
+ const anchorRect = anchorEl.getBoundingClientRect();
+ const menuRect = menuRef.current.getBoundingClientRect();
+ const scrollY = window.scrollY || window.pageYOffset;
+ const scrollX = window.scrollX || window.pageXOffset;
+
+ let left = anchorRect.right + scrollX - menuRect.width;
+ let top = anchorRect.bottom + scrollY;
+
+ if (left < 0) left = scrollX + 4;
+ if (top + menuRect.height > window.innerHeight + scrollY) {
+ top = anchorRect.top + scrollY - menuRect.height;
+ }
+
+ setStylePos({
+ position: "absolute",
+ top: `${Math.round(top)}px`,
+ left: `${Math.round(left)}px`,
+ visibility: "visible",
+ zIndex: 1000,
+ });
+ }, [isOpen, anchorEl, children]);
+
+ useEffect(() => {
+ if (!isOpen) return;
+
+ const handleDocClick = (e) => {
+ if (menuRef.current && !menuRef.current.contains(e.target) &&
+ anchorEl && !anchorEl.contains(e.target)) {
+ onClose();
+ }
+ };
+ const handleScroll = () => onClose();
+
+ document.addEventListener("mousedown", handleDocClick);
+ document.addEventListener("scroll", handleScroll, true);
+
+ return () => {
+ document.removeEventListener("mousedown", handleDocClick);
+ document.removeEventListener("scroll", handleScroll, true);
+ };
+ }, [isOpen, onClose, anchorEl]);
+
+ if (!isOpen) return null;
+ return createPortal(
+
e.stopPropagation()}>
+ {children}
+
,
+ document.body
+ );
+}
+
+function LaudoList() {
+ const [search, setSearch] = useState("");
+ const [period, setPeriod] = useState(""); // "", "today", "week", "month"
+ const [startDate, setStartDate] = useState("");
+ const [endDate, setEndDate] = useState("");
+ const [statusFilter, setStatusFilter] = useState("");
+ const [laudos, setLaudos] = useState([])
+ const [openDropdown, setOpenDropdown] = useState(null);
+ const anchorRefs = useRef({});
+ const tokenUsuario = getAccessToken()
+ const role = getUserRole();
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || "https://yuanqfswhberkoevtmfr.supabase.co";
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY || "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
+
+ var myHeaders = new Headers();
+ myHeaders.append(
+ "apikey",
+ supabaseAK
+ );
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+ var requestOptions = {
+ method: 'GET',
+ headers: myHeaders,
+ redirect: 'follow'
+ };
+
+ useEffect(() => {
+ fetch(`${supabaseUrl}/rest/v1/reports`, requestOptions)
+ .then(response => response.json())
+ .then(result => setLaudos(Array.isArray(result) ? result : []))
+ .catch(error => console.log('error', error));
+ }, [])
+
+
+
+ const handleVerDetalhes = (laudo) => {
+ Swal.fire({
+ title: "Detalhes do Laudo",
+ html: `
+
+
+
Informações do Pedido
+
Nº Pedido: ${laudo.order_number || 'N/A'}
+
Paciente ID: ${laudo.patient_id || 'N/A'}
+
Tipo: ${laudo.tipo || 'N/A'}
+
+
+
+
Detalhes do Exame
+
Exame: ${laudo.exam || 'N/A'}
+
Diagnóstico: ${laudo.diagnosis || 'Nenhum diagnóstico'}
+
Conclusão: ${laudo.conclusion || 'Nenhuma conclusão'}
+
+
+
+
Responsáveis
+
Executante: ${laudo.requested_by || 'N/A'}
+
+
+
+
Datas
+
Criado em: ${formatDate(laudo.created_at) || 'N/A'}
+
+
+ `,
+ showCancelButton: true,
+ confirmButtonText: "Abrir Laudo",
+ cancelButtonText: "Fechar",
+ confirmButtonColor: "#3085d6",
+ cancelButtonColor: "#6c757d",
+ icon: "info",
+ width: "600px",
+ draggable: true
+ }).then((result) => {
+ if (result.isConfirmed) {
+ // Abrir o form de laudo
+ abrirFormLaudo(laudo.id);
+ }
+ });
+ };
+
+ const abrirFormLaudo = (laudoId) => {
+ // Navega para o form de laudo com o ID
+ window.location.href = `/${role}/laudoform?id=${laudoId}`;
+ };
+
+ const getStatusBadgeClass = (status) => {
+ switch (status?.toLowerCase()) {
+ case 'concluído':
+ case 'finalizado':
+ return 'bg-success';
+ case 'pendente':
+ return 'bg-warning';
+ case 'cancelado':
+ return 'bg-danger';
+ default:
+ return 'bg-secondary';
+ }
+ };
+
+ const formatDate = (dateString) => {
+ if (!dateString) return 'N/A';
+ try {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('pt-BR', {
+ day: '2-digit',
+ month: '2-digit',
+ year: 'numeric',
+ hour: '2-digit',
+ minute: '2-digit'
+ }).replace(',', ' às');
+ } catch {
+ return dateString;
+ }
+ };
+
+ const handleDelete = (id) => {
+ Swal.fire({
+ title: "Tem certeza?",
+ text: "Tem certeza que deseja excluir este laudo?",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#e63946",
+ cancelButtonColor: "#6c757d",
+ confirmButtonText: "Excluir!",
+ cancelButtonText: "Cancelar",
+ }).then((result) => {
+ if (result.isConfirmed) {
+
+ var requestOptions = {
+ method: 'DELETE',
+ headers: myHeaders,
+ redirect: 'follow'
+ };
+
+ fetch(`${supabaseUrl}/rest/v1/reports?id=eq.${id}`, requestOptions)
+ .then(response => response.text())
+ .then(result => console.log(result))
+ .catch(error => console.log('error', error));
+ setLaudos(prev => prev.filter(l => l.id !== id));
+ setOpenDropdown(null);
+ Swal.fire({
+ title: "Excluído!",
+ text: "Laudo excluído com sucesso.",
+ icon: "success",
+ draggable: true
+ });
+ }
+ });
+ };
+
+ const mascararCPF = (cpf = "") => {
+ if (cpf.length < 5) return cpf;
+ return `${cpf.slice(0, 3)}.***.***-${cpf.slice(-2)}`;
+ };
+ const [pacientesMap, setPacientesMap] = useState({});
+ // useEffect para atualizar todos os nomes
+ useEffect(() => {
+ if (!laudos || laudos.length === 0) return;
+
+ const buscarPacientes = async () => {
+ try {
+ // Pega IDs únicos de pacientes
+ const idsUnicos = [...new Set(laudos.map((l) => l.patient_id))];
+
+ // Faz apenas 1 fetch por paciente
+ const promises = idsUnicos.map(async (id) => {
+ try {
+ const res = await fetch(
+ `${supabaseUrl}/rest/v1/patients?id=eq.${id}`,
+ {
+ method: "GET",
+ headers: {
+ apikey:
+ supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ }
+ );
+ const data = await res.json();
+ return { id, full_name: data[0]?.full_name || "Nome não encontrado" };
+ } catch (err) {
+ return { id, full_name: "Nome não encontrado" };
+ }
+ });
+
+ const results = await Promise.all(promises);
+
+ const map = {};
+ results.forEach((r) => (map[r.id] = r.full_name));
+ setPacientesMap(map);
+ } catch (err) {
+ console.error("Erro ao buscar pacientes:", err);
+ }
+ };
+
+ buscarPacientes();
+ }, [laudos]);
+ const filteredLaudos = laudos.filter(l => {
+ const q = search.toLowerCase();
+ const textMatch =
+ (pacientesMap[l.patient_id]?.toLowerCase() || "").includes(q) ||
+ (l.status || "").toLowerCase().includes(q) ||
+ (l.order_number || "").toString().toLowerCase().includes(q) ||
+ (l.exam || "").toLowerCase().includes(q) ||
+ (l.diagnosis || "").toLowerCase().includes(q) ||
+ (l.conclusion || "").toLowerCase().includes(q);
+
+ // Filtro por status
+ const matchesStatus = !statusFilter || l.status === statusFilter;
+
+ let dateMatch = true;
+ if (l.created_at) {
+ const laudoDate = new Date(l.created_at);
+ const today = new Date();
+
+ // Filtros por período rápido
+ if (period === "today") {
+ const todayStr = today.toDateString();
+ dateMatch = laudoDate.toDateString() === todayStr;
+ } else if (period === "week") {
+ const startOfWeek = new Date(today);
+ startOfWeek.setDate(today.getDate() - today.getDay());
+ startOfWeek.setHours(0, 0, 0, 0);
+ const endOfWeek = new Date(startOfWeek);
+ endOfWeek.setDate(startOfWeek.getDate() + 6);
+ endOfWeek.setHours(23, 59, 59, 999);
+ dateMatch = laudoDate >= startOfWeek && laudoDate <= endOfWeek;
+ } else if (period === "month") {
+ dateMatch = laudoDate.getMonth() === today.getMonth() &&
+ laudoDate.getFullYear() === today.getFullYear();
+ }
+
+ // Filtros por data específica
+ if (startDate && endDate) {
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+ end.setHours(23, 59, 59, 999); // Inclui o dia inteiro
+ dateMatch = dateMatch && laudoDate >= start && laudoDate <= end;
+ } else if (startDate) {
+ const start = new Date(startDate);
+ dateMatch = dateMatch && laudoDate >= start;
+ } else if (endDate) {
+ const end = new Date(endDate);
+ end.setHours(23, 59, 59, 999);
+ dateMatch = dateMatch && laudoDate <= end;
+ }
+ }
+
+ return textMatch && matchesStatus && dateMatch;
+ });
+
+ const [itemsPerPage1, setItemsPerPage1] = useState(15);
+ const [currentPage1, setCurrentPage1] = useState(1);
+ const indexOfLastLaudos = currentPage1 * itemsPerPage1;
+ const indexOfFirstLaudos = indexOfLastLaudos - itemsPerPage1;
+ const currentLaudos = filteredLaudos.slice(indexOfFirstLaudos, indexOfLastLaudos);
+ const totalPages1 = Math.ceil(filteredLaudos.length / itemsPerPage1);
+ const navigate = useNavigate();
+ const [medicosMap, setMedicosMap] = useState({});
+ // Função para definir períodos e limpar datas
+ const handlePeriodChange = (newPeriod) => {
+ // Se clicar no mesmo período, limpa o filtro
+ if (period === newPeriod) {
+ setPeriod("");
+ } else {
+ setPeriod(newPeriod);
+ }
+
+ // Sempre limpa as datas específicas
+ setStartDate("");
+ setEndDate("");
+ };
+
+ useEffect(() => {
+ setCurrentPage1(1);
+ }, [search, statusFilter, period, startDate, endDate]);
+
+ useEffect(() => {
+ if (!Array.isArray(laudos) || laudos.length === 0) return;
+
+ const buscarMedicos = async () => {
+ try {
+ const idsUnicos = [...new Set(laudos.map((c) => c.requested_by).filter(Boolean))];
+ if (idsUnicos.length === 0) return;
+
+ const headers = {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${tokenUsuario}`,
+ apikey: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ",
+ };
+
+ const promises = idsUnicos.map(async (id) => {
+ try {
+ const res = await fetch(`https://yuanqfswhberkoevtmfr.supabase.co/rest/v1/doctors?id=eq.${id}`, {
+ method: "GET",
+ headers,
+ });
+ if (!res.ok) return { id, full_name: "Nome não encontrado" };
+ const data = await res.json();
+ return { id, full_name: data?.[0]?.full_name || "Nome não encontrado" };
+ } catch {
+ return { id, full_name: "Nome não encontrado" };
+ }
+ });
+
+ const results = await Promise.all(promises);
+ const map = {};
+ results.forEach((r) => (map[r.id] = r.full_name));
+ setMedicosMap(map);
+ } catch (err) {
+ console.error("Erro ao buscar nomes dos médicos:", err);
+ }
+ };
+
+ buscarMedicos();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [laudos]);
+ const permissoes = {
+ admin: ['editlaudo', 'deletarlaudo', 'viewlaudo', 'viewpatientlaudos', 'createlaudo', 'executantelaudo'],
+ medico: ['editlaudo', 'deletarlaudo', 'viewlaudo', 'viewpatientlaudos', 'createlaudo'],
+ paciente: ['viewlaudo']
+};
+ const pode = (acao) => permissoes[role]?.includes(acao);
+ // Função para imprimir o laudo (content_html)
+ const handlePrint = async (laudoId) => {
+ try {
+ const res = await fetch(`${supabaseUrl}/rest/v1/reports?id=eq.${laudoId}`, {
+ method: 'GET',
+ headers: myHeaders,
+ });
+ if (!res.ok) {
+ Swal.fire({
+ icon: 'error',
+ title: 'Erro de autenticação',
+ text: 'Não foi possível acessar o laudo. Faça login novamente.'
+ });
+ return;
+ }
+ const data = await res.json();
+ const contentHtml = data[0]?.content_html;
+ if (!contentHtml) {
+ Swal.fire({
+ icon: 'warning',
+ title: 'Sem conteúdo',
+ text: 'Este laudo não possui conteúdo para impressão.'
+ });
+ return;
+ }
+ // Caminho da logo (ajuste se necessário)
+ const logoUrl = '/public/img/logomedconnect.png';
+ const pacienteNome = pacientesMap[data[0]?.patient_id] || 'Paciente';
+ const pedido = data[0]?.order_number || 'N/A';
+ const exame = data[0]?.exam || 'N/A';
+ const dataCriacao = formatDate(data[0]?.created_at);
+ const medicoNome = medicosMap[data[0]?.requested_by] || data[0]?.requested_by || '';
+ const printWindow = window.open('', '', 'width=900,height=700');
+ printWindow.document.write(`
+
+
+
+
Laudo Médico - ${pacienteNome}
+
+
+
+
+
+ Paciente: ${pacienteNome}
+ Pedido: ${pedido}
+ Exame: ${exame}
+ Médico: ${medicoNome}
+ Data: ${dataCriacao}
+
+
Laudo Médico
+
${contentHtml}
+
+
+
+ `);
+ printWindow.document.close();
+ printWindow.focus();
+ printWindow.print();
+ } catch (err) {
+ Swal.fire({
+ icon: 'error',
+ title: 'Erro ao imprimir',
+ text: 'Não foi possível imprimir o laudo.'
+ });
+ }
+ };
+ return (
+
+
+ {/* Header com título e botão */}
+
+
+
Laudos
+ {pode('createlaudo') && (
+ {
+ e.stopPropagation();
+ setOpenDropdown(null);
+ }}
+ className="btn btn-primary btn-rounded"
+ >
+ Adicionar Laudo
+
+ )}
+
+
+
+ {/* Todos os filtros em uma única linha */}
+
+ {/* Campo de busca */}
+
setSearch(e.target.value)}
+ style={{ minWidth: "300px", maxWidth: "450px", }}
+ />
+
+ {/* Filtro de status */}
+
setStatusFilter(e.target.value)}
+ >
+
+
+
+
+
+ {/* Filtro De */}
+
+
+ {
+ setStartDate(e.target.value);
+ if (e.target.value) setPeriod("");
+ }}
+ />
+
+
+ {/* Filtro Até */}
+
+
+ {
+ setEndDate(e.target.value);
+ if (e.target.value) setPeriod("");
+ }}
+ />
+
+
+ {/* Botões rápidos */}
+
+
+
+
+
+ {/* Tabela */}
+
+
+
+
+
+
+ | Pedido |
+ {pode('viewpatientlaudos') && (
+ Paciente |
+ )}
+ Procedimento |
+ Diagnóstico |
+ Conclusão |
+ Status |
+ {pode('executantelaudo') && (
+ Executante |
+ )}
+ Criado em |
+ Ações |
+
+
+
+ {currentLaudos.length > 0 ? currentLaudos.map(l => (
+
+ | {l.order_number} |
+ {pode('viewpatientlaudos') && (
+ {pacientesMap[l.patient_id] || "Carregando..."} |
+ )}
+ {l.exam} |
+ {l.diagnosis} |
+ {l.conclusion} |
+
+
+ {l.status === 'draft' ? (
+ <>
+
+ Rascunho
+ >
+ ) : l.status === 'completed' ? (
+ <>
+
+ Concluído
+ >
+ ) : (
+ l.status
+ )}
+
+ |
+ {pode('executantelaudo') && (
+ {medicosMap[l.requested_by] || l.requested_by} |
+ )}
+ {formatDate(l.created_at)} |
+
+
+ {pode('editlaudo') && (
+ )}
+
+ {/* Botão de imprimir */}
+
+ {pode('deletarlaudo') && (
+
+ )}
+
+ |
+
+ )) : (
+
+ | Nenhum laudo encontrado |
+
+ )}
+
+
+
+
+
+ Total encontrados: {filteredLaudos.length}
+
+
+ {
+ setItemsPerPage1(Number(e.target.value));
+ setCurrentPage1(1);
+ }}
+ title="Itens por página"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default LaudoList;
\ No newline at end of file
diff --git a/src/components/lists/PatientList.jsx b/src/components/lists/PatientList.jsx
new file mode 100644
index 0000000..daf26cd
--- /dev/null
+++ b/src/components/lists/PatientList.jsx
@@ -0,0 +1,616 @@
+import { Link } from "react-router-dom";
+import "../../assets/css/index.css";
+import React, { useState, useEffect, useRef, useLayoutEffect } from "react";
+import { createPortal } from "react-dom";
+import supabase from "../../Supabase.js";
+import { getAccessToken } from "../../utils/auth.js";
+import Swal from "sweetalert2";
+import '../../assets/css/modal-details.css';
+const AvatarForm = "/img/AvatarForm.jpg";
+import { useNavigate } from "react-router-dom";
+import { getUserRole } from "../../utils/userInfo.js";
+// Componente que renderiza o menu em um portal (document.body) e posiciona em relação ao botão
+function DropdownPortal({ anchorEl, isOpen, onClose, className, children }) {
+ const menuRef = useRef(null);
+ const [stylePos, setStylePos] = useState({
+ position: "absolute",
+ top: 0,
+ left: 0,
+ visibility: "hidden",
+ zIndex: 1000,
+ });
+
+ // Posiciona o menu após renderar (medir tamanho do menu)
+ useLayoutEffect(() => {
+ if (!isOpen) return;
+ if (!anchorEl || !menuRef.current) return;
+
+ const anchorRect = anchorEl.getBoundingClientRect();
+ const menuRect = menuRef.current.getBoundingClientRect();
+ const scrollY = window.scrollY || window.pageYOffset;
+ const scrollX = window.scrollX || window.pageXOffset;
+
+ // tenta alinhar à direita do botão (como dropdown-menu-right)
+ let left = anchorRect.right + scrollX - menuRect.width;
+ let top = anchorRect.bottom + scrollY;
+
+ // evita sair da esquerda da tela
+ if (left < 0) left = scrollX + 4;
+ // se extrapolar bottom, abre para cima
+ if (top + menuRect.height > window.innerHeight + scrollY) {
+ top = anchorRect.top + scrollY - menuRect.height;
+ }
+ setStylePos({
+ position: "absolute",
+ top: `${Math.round(top)}px`,
+ left: `${Math.round(left)}px`,
+ visibility: "visible",
+ zIndex: 1000,
+ });
+ }, [isOpen, anchorEl, children]);
+
+ // fecha ao clicar fora / ao rolar
+ useEffect(() => {
+ if (!isOpen) return;
+ function handleDocClick(e) {
+ const menu = menuRef.current;
+ if (menu && !menu.contains(e.target) && anchorEl && !anchorEl.contains(e.target)) {
+ onClose();
+ }
+ }
+ function handleScroll() {
+ onClose();
+ }
+ document.addEventListener("mousedown", handleDocClick);
+ // captura scroll em qualquer elemento (true)
+ document.addEventListener("scroll", handleScroll, true);
+ return () => {
+ document.removeEventListener("mousedown", handleDocClick);
+ document.removeEventListener("scroll", handleScroll, true);
+ };
+ }, [isOpen, onClose, anchorEl]);
+
+ if (!isOpen) return null;
+ return createPortal(
+
e.stopPropagation()}
+ >
+ {children}
+
,
+ document.body
+ );
+}
+
+function PatientList() {
+ const [search, setSearch] = useState("");
+ const [sexFilter, setSexFilter] = useState(""); // Filtro por sexo
+ const [patients, setPatients] = useState([]);
+ const [openDropdown, setOpenDropdown] = useState(null);
+ const anchorRefs = useRef({}); // guarda referência do botão de cada linha
+ // 🟢 ADICIONADO — controla o modal e o paciente selecionado
+ const [selectedPatient, setSelectedPatient] = useState(null);
+ const [showModal, setShowModal] = useState(false);
+ const role = getUserRole();
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const handleViewDetails = (patient) => {
+ const mascararCPF = (cpf = "") => {
+ if (cpf.length < 5) return cpf;
+ const inicio = cpf.slice(0, 3);
+ const fim = cpf.slice(-2);
+ return `${inicio}.***.***-${fim}`;
+ };
+
+ Swal.fire({
+ title: `
Detalhes do Paciente
`,
+ html: `
+
+

+
${patient.full_name}
+
Informações detalhadas sobre o paciente.
+
+
+
+
+
+
Nome Completo: ${patient.full_name}
+
Telefone: ${patient.phone_mobile}
+
CPF: ${mascararCPF(patient.cpf)}
+
Peso (kg): ${patient.weight || "—"}
+
Endereço: ${patient.address || "—"}
+
+
+
Email: ${patient.email}
+
Data de Nascimento: ${patient.birth_date}
+
Tipo Sanguíneo: ${patient.blood_type || "—"}
+
Altura (m): ${patient.height || "—"}
+
+
+
+ `,
+ width: "800px",
+ showConfirmButton: true,
+ confirmButtonText: "Fechar",
+ confirmButtonColor: "#4dabf7",
+ background: document.body.classList.contains("dark-mode")
+ ? "#1e1e2f"
+ : "#fff",
+ color: document.body.classList.contains("dark-mode")
+ ? "#f5f5f5"
+ : "#000",
+ customClass: {
+ popup: 'swal2-modal-patient'
+ }
+ });
+ };
+
+
+ const tokenUsuario = getAccessToken()
+ var myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+ var requestOptions = {
+ method: 'GET',
+ headers: myHeaders,
+ redirect: 'follow'
+ };
+ useEffect(() => {
+ fetch(`${supabaseUrl}/rest/v1/patients`, requestOptions)
+ .then(response => response.json())
+ .then(result => {
+ setPatients(Array.isArray(result) ? result : [])
+ console.log(result);
+ })
+ .catch(error => console.log('error', error));
+ }, [])
+
+ const handleDelete = async (id) => {
+ if (getUserRole() === 'paciente' || getUserRole() === 'medico') {
+ Swal.fire("Ação não permitida", "Pacientes e médicos não podem excluir pacientes. Por favor, entre em contato com a secretaria.", "warning");
+ return;
+ }
+ Swal.fire({
+ title: "Tem certeza?",
+ text: "Tem certeza que deseja excluir este paciente?",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonColor: "#e63946",
+ cancelButtonColor: "#6c757d",
+ confirmButtonText: "Excluir!",
+ cancelButtonText: "Cancelar",
+ }).then(async (result) => {
+ if (result.isConfirmed) {
+ try {
+ var myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+
+ var requestOptions = {
+ method: 'DELETE',
+ headers: myHeaders,
+ redirect: 'follow'
+ };
+
+ const response = await fetch(`${supabaseUrl}/rest/v1/patients?id=eq.${id}`, requestOptions)
+
+ if (response.ok) {
+ setPatients(prev => prev.filter(l => l.id !== id));
+ setOpenDropdown(null);
+ Swal.fire({
+ title: "Registro Excluído",
+ text: "Registro excluído com sucesso",
+ icon: "success"
+ })
+
+ } else {
+ Swal.fire("Error saving changes", "", "error");
+ }
+ }
+ catch (error) {
+ Swal.fire("Something went wrong", "", "error");
+ console.error(error);
+ }
+ }
+ });
+ };
+
+
+ const [birthDateStart, setBirthDateStart] = useState("");
+ const [birthDateEnd, setBirthDateEnd] = useState("");
+ const [ageRange, setAgeRange] = useState("");
+ const [bloodType, setBloodType] = useState("");
+
+ const filteredPatients = patients.filter(p => {
+ if (!p) return false;
+
+ // Filtro por texto (nome, cpf, email)
+ const nome = (p.full_name || "").toLowerCase();
+ const cpf = (p.cpf || "").toLowerCase();
+ const email = (p.email || "").toLowerCase();
+ const data = (p.birth_date || "").toLowerCase();
+ const q = search.toLowerCase();
+ const matchesSearch = nome.includes(q) || cpf.includes(q) || email.includes(q) || data.includes(q);
+
+ // Filtro por sexo (flexível - aceita diferentes variações)
+ let matchesSex = true;
+ if (sexFilter) {
+ const patientSex = (p.sex || "").toLowerCase().trim();
+
+ if (sexFilter === "masculino") {
+ matchesSex = patientSex === "masculino" || patientSex === "m" || patientSex === "male";
+ } else if (sexFilter === "feminino") {
+ matchesSex = patientSex === "feminino" || patientSex === "f" || patientSex === "female";
+ } else if (sexFilter === "outros") {
+ matchesSex = !["masculino", "m", "male", "feminino", "f", "female", ""].includes(patientSex);
+ }
+ }
+
+ // Filtro por data de nascimento
+ let matchesBirthDate = true;
+ if (birthDateStart) {
+ matchesBirthDate = p.birth_date >= birthDateStart;
+ }
+ if (matchesBirthDate && birthDateEnd) {
+ matchesBirthDate = p.birth_date <= birthDateEnd;
+ }
+
+ // Filtro por faixa etária
+ let matchesAge = true;
+ if (ageRange && p.birth_date) {
+ const today = new Date();
+ const birth = new Date(p.birth_date);
+ let age = today.getFullYear() - birth.getFullYear();
+ const m = today.getMonth() - birth.getMonth();
+ if (m < 0 || (m === 0 && today.getDate() < birth.getDate())) {
+ age--;
+ }
+ if (ageRange === "0-18") matchesAge = age >= 0 && age <= 18;
+ else if (ageRange === "19-40") matchesAge = age >= 19 && age <= 40;
+ else if (ageRange === "41-60") matchesAge = age >= 41 && age <= 60;
+ else if (ageRange === "60+") matchesAge = age > 60;
+ }
+
+ // Filtro por tipo sanguíneo
+ let matchesBlood = true;
+ if (bloodType) {
+ matchesBlood = (p.blood_type || "").toUpperCase() === bloodType;
+ }
+ return matchesSearch && matchesSex && matchesBirthDate && matchesAge && matchesBlood;
+ });
+ const [itemsPerPage1, setItemsPerPage1] = useState(15);
+ const [currentPage1, setCurrentPage1] = useState(1);
+ const indexOfLastPatient = currentPage1 * itemsPerPage1;
+ const indexOfFirstPatient = indexOfLastPatient - itemsPerPage1;
+ const currentPatients = filteredPatients.slice(indexOfFirstPatient, indexOfLastPatient);
+ const totalPages1 = Math.ceil(filteredPatients.length / itemsPerPage1);
+ useEffect(() => {
+ setCurrentPage1(1);
+ }, [search, sexFilter]);
+
+ const mascararCPF = (cpf = "") => {
+ if (cpf.length < 5) return cpf;
+ const inicio = cpf.slice(0, 3);
+ const fim = cpf.slice(-2);
+ return `${inicio}.***.***-${fim}`;
+ };
+
+ const renderSexBadge = (sex) => {
+ const sexo = (sex || "").toLowerCase().trim();
+
+ if (sexo === "masculino" || sexo === "m" || sexo === "male") {
+ return (
+
+
+ Masculino
+
+ );
+ } else if (sexo === "feminino" || sexo === "f" || sexo === "female") {
+ return (
+
+
+ Feminino
+
+ );
+ } else if (sexo === "") {
+ return (
+
+
+ Em branco
+
+ );
+ } else {
+ return (
+
+
+ {sexo || "Outros"}
+
+ );
+ }
+ };
+
+ const navigate = useNavigate();
+ const permissoes = {
+ admin: ['editpatient', 'deletepatient'],
+ medico: [''],
+ secretaria: ['editpatient', 'deletepatient'],
+ };
+ const pode = (acao) => permissoes[role]?.includes(acao);
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ | Nome |
+ Cpf |
+ Data de Nascimento |
+ Telefone |
+ Email |
+ Sexo |
+ Ações |
+
+
+
+ {currentPatients.length > 0 ? (
+ currentPatients.map((p) => (
+
+
+
+
+  {
+ e.target.src = AvatarForm; // Fallback se a imagem não carregar
+ }}
+ />
+ {p.full_name}
+
+
+ |
+ {mascararCPF(p.cpf)} |
+ {p.birth_date} |
+ {p.phone_mobile} |
+ {p.email} |
+ {renderSexBadge(p.sex)} |
+
+
+
+
+ {pode('editpatient') && (
+
+ )}
+ {pode('deletepatient') && (
+
+ )}
+
+
+ |
+
+ ))
+ ) : (
+
+ |
+ Nenhum paciente encontrado
+ |
+
+ )}
+
+
+
+ {/* Linha de controles abaixo da tabela */}
+
+
+ Total encontrados: {filteredPatients.length}
+
+
+ {
+ setItemsPerPage1(Number(e.target.value));
+ setCurrentPage1(1);
+ }}
+ title="Itens por página"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default PatientList;
\ No newline at end of file
diff --git a/src/main.jsx b/src/main.jsx
index b509f92..0765cc9 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -1,81 +1,13 @@
-// src/main.jsx
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { createBrowserRouter, RouterProvider } from "react-router-dom";
-
-import "./assets/css/index.css";
-
-// Layouts
-import App from "./App.jsx"; // Layout Admin
-import DoctorApp from "./pages/DoctorApp/DoctorApp.jsx"; // Layout Médico
-
-// Páginas Admin
-import Patientform from "./pages/Patient/Patientform.jsx";
-import PatientList from "./pages/Patient/PatientList.jsx";
-import Doctorlist from "./pages/Doctor/DoctorList.jsx";
-import DoctorForm from "./pages/Doctor/DoctorForm.jsx";
-import Doctorschedule from "./pages/Schedule/DoctorSchedule.jsx";
-import AddSchedule from "./pages/Schedule/AddSchedule.jsx";
-import Calendar from "./pages/calendar/Calendar.jsx";
-import EditDoctor from "./pages/Doctor/DoctorEdit.jsx";
-import PatientEdit from "./pages/Patient/PatientEdit.jsx";
-import DoctorProfile from "./pages/Doctor/DoctorProfile.jsx";
-
-
-// Páginas Médico
-import DoctorDashboard from "./pages/DoctorApp/DoctorDashboard.jsx";
-import DoctorCalendar from "./pages/DoctorApp/DoctorCalendar.jsx";
-import DoctorPatientList from "./pages/DoctorApp/DoctorPatientList.jsx";
-import AgendaList from "./pages/Agendar/AgendaList.jsx";
-import AgendaForm from "./pages/Agendar/AgendaForm.jsx";
-import AgendaEdit from "./pages/Agendar/AgendaEdit.jsx";
-import LaudoList from "./pages/laudos/LaudosList.jsx"
-import Laudo from "./pages/laudos/Laudo.jsx";
-
-
-
+import { router } from "./routes/RoutesApp.jsx";
// Criando o router com todas as rotas
-const router = createBrowserRouter([
- // Rotas Admin
- {
- path: "/",
- element:
,
- children: [
- // Rota inicial do Admin: apenas mostra layout com Navbar e Sidebar
- { path: "patient", element:
},
- { path: "patientlist", element:
},
- { path: "doctorlist", element:
},
- { path: "doctorform", element:
},
- { path: "doctorschedule", element:
},
- { path: "addschedule", element:
},
- { path: "calendar", element:
},
- { path: "profiledoctor/:id", element:
},
- { path: "editdoctor/:id", element:
},
- { path: "editpatient/:id", element:
},
- { path: "agendaform", element:
},
- { path: "agendaedit", element:
},
- { path: "agendalist", element:
},
- { path: "laudolist", element:
},
- { path: "laudo", element:
}
- ],
- },
- // Rotas Médico
- {
- path: "/doctor",
- element:
,
- children: [
- { index: true, element:
}, // Rota inicial médico
- { path: "dashboard", element:
},
- { path: "calendar", element:
},
- { path: "patients", element:
},
- ],
- },
-]);
-// Renderizando a aplicação
+
createRoot(document.getElementById("root")).render(
-);
+);
\ No newline at end of file
diff --git a/src/pages/AdminApp/AdminApp.jsx b/src/pages/AdminApp/AdminApp.jsx
new file mode 100644
index 0000000..c72984b
--- /dev/null
+++ b/src/pages/AdminApp/AdminApp.jsx
@@ -0,0 +1,49 @@
+import '../../assets/css/index.css'
+import { Link, useLocation } from 'react-router-dom';
+import { useState } from 'react';
+import { Outlet } from 'react-router-dom';
+import { getAccessToken } from '../../utils/auth.js';
+import { getUserRole } from '../../utils/userInfo.js';
+import Sidebar from '../../components/layouts/Sidebar.jsx';
+
+
+
+
+function AdminApp() {
+ const token = getAccessToken();
+ const user = getUserRole();
+ // Verificação de autenticação
+ if (!token) {
+ return
;
+ }
+
+ // Verificação de role
+ if (user !== 'admin') {
+ return (
+
+
+
+
❌ Acesso Negado
+
Apenas administradores podem acessar esta área.
+
+
+
+
+ );
+ }
+ return (
+
+
+
+
+ );
+}
+
+export { AdminApp };
+export default AdminApp;
+
diff --git a/src/pages/AdminApp/AdminDashboard.jsx b/src/pages/AdminApp/AdminDashboard.jsx
new file mode 100644
index 0000000..02c4e77
--- /dev/null
+++ b/src/pages/AdminApp/AdminDashboard.jsx
@@ -0,0 +1,874 @@
+import React, { useState, useEffect } from "react";
+import { Link } from "react-router-dom";
+import { getAccessToken } from "../../utils/auth.js";
+import "../../assets/css/index.css";
+import Tabs from '@mui/material/Tabs';
+import Tab from '@mui/material/Tab';
+import Box from '@mui/material/Box';
+import { styled } from '@mui/material/styles';
+import { getFullName, getUserId } from "../../utils/userInfo.js";
+// Usando URLs das imagens no public
+const AvatarForm = "/img/AvatarForm.jpg";
+const banner = "/img/banner.png";
+import {
+ BarChart,
+ Bar,
+ PieChart,
+ Pie,
+ Cell,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer
+} from 'recharts';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip as ChartTooltip,
+ Legend as ChartLegend,
+} from 'chart.js';
+import { Bar as ChartJSBar } from 'react-chartjs-2';
+
+// Registrar componentes do Chart.js
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ ChartTooltip,
+ ChartLegend
+);
+
+// Componente do gráfico de consultas mensais
+const ConsultasMensaisChart = ({ data }) => (
+
+
+
+
+
+ [`${value} consultas`, 'Total']}
+ />
+
+
+
+
+);
+
+// Componente do gráfico de pacientes ativos/inativos
+const AtivosInativosChart = ({ data }) => (
+
+
+ `${name} ${(percent * 100).toFixed(0)}%`}
+ outerRadius={120}
+ fill="#8884d8"
+ dataKey="value"
+ >
+ {data.map((entry, index) => (
+ |
+ ))}
+
+ [`${value} pacientes`, name]}
+ />
+
+
+
+);
+
+// Componente do gráfico de taxa de cancelamentos
+const TaxaCancelamentosChart = ({ data }) => {
+
+
+ if (!data || data.length === 0) {
+ return (
+
+
+
+
Nenhum dado de cancelamentos encontrado
+
+
+ );
+ }
+
+ // Preparar dados para Chart.js (gráfico empilhado)
+ const chartData = {
+ labels: data.map(item => item.mes),
+ datasets: [
+ {
+ label: 'Realizadas',
+ data: data.map(item => item.realizadas),
+ backgroundColor: '#dee2e6',
+ borderColor: '#adb5bd',
+ borderWidth: 1,
+ borderRadius: 4,
+ borderSkipped: false,
+ },
+ {
+ label: 'Canceladas',
+ data: data.map(item => item.canceladas),
+ backgroundColor: '#dc3545',
+ borderColor: '#c82333',
+ borderWidth: 1,
+ borderRadius: 4,
+ borderSkipped: false,
+ }
+ ]
+ };
+
+ const options = {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ stacked: true,
+ grid: {
+ display: false,
+ },
+ ticks: {
+ color: '#6c757d',
+ font: {
+ size: 12
+ }
+ }
+ },
+ y: {
+ stacked: true,
+ beginAtZero: true,
+ max: 100,
+ grid: {
+ color: '#e9ecef',
+ drawBorder: false,
+ },
+ ticks: {
+ color: '#6c757d',
+ font: {
+ size: 12
+ },
+ callback: function(value) {
+ return value + '%';
+ }
+ }
+ }
+ },
+ plugins: {
+ legend: {
+ display: true,
+ position: 'top',
+ labels: {
+ color: '#495057',
+ font: {
+ size: 12
+ },
+ usePointStyle: true,
+ pointStyle: 'rect'
+ }
+ },
+ tooltip: {
+ backgroundColor: '#f8f9fa',
+ titleColor: '#343a40',
+ bodyColor: '#343a40',
+ borderColor: '#dee2e6',
+ borderWidth: 1,
+ callbacks: {
+ label: function(context) {
+ const datasetLabel = context.dataset.label;
+ const value = context.parsed.y;
+ const dataIndex = context.dataIndex;
+ const monthData = data[dataIndex];
+
+ if (datasetLabel === 'Canceladas') {
+ const numConsultas = Math.round(monthData.total * value / 100);
+ return `${datasetLabel}: ${value}% (${numConsultas} de ${monthData.total} consultas)`;
+ } else {
+ const numConsultas = Math.round(monthData.total * value / 100);
+ return `${datasetLabel}: ${value}% (${numConsultas} consultas)`;
+ }
+ },
+ title: function(context) {
+ const monthData = data[context[0].dataIndex];
+ return `${context[0].label} ${new Date().getFullYear()} - Total: ${monthData.total} consultas`;
+ },
+ afterBody: function(context) {
+ const monthData = data[context[0].dataIndex];
+ if (monthData.total === 0) {
+ return ['Nenhuma consulta registrada neste mês'];
+ }
+ return [];
+ }
+ }
+ }
+ },
+ animation: {
+ duration: 1000,
+ easing: 'easeInOutQuart'
+ },
+ layout: {
+ padding: {
+ left: 10,
+ right: 10,
+ top: 10,
+ bottom: 10
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+// Componente do gráfico de consultas por médico com Chart.js (horizontal)
+const ConsultasPorMedicoChart = ({ data }) => {
+
+
+ if (!data || data.length === 0) {
+ return (
+
+
+
+
Nenhum dado de médicos encontrado
+
+
+ );
+ }
+
+ // Preparar dados para Chart.js
+ const chartData = {
+ labels: data.map(item => item.medico),
+ datasets: [
+ {
+ label: 'Consultas',
+ data: data.map(item => item.consultas),
+ backgroundColor: '#28a745',
+ borderColor: '#1e7e34',
+ borderWidth: 1,
+ borderRadius: 4,
+ borderSkipped: false,
+ }
+ ]
+ };
+
+ const options = {
+ indexAxis: 'y', // Torna o gráfico horizontal
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false, // Ocultar legenda pois é óbvio
+ },
+ tooltip: {
+ backgroundColor: '#f8f9fa',
+ titleColor: '#343a40',
+ bodyColor: '#343a40',
+ borderColor: '#dee2e6',
+ borderWidth: 1,
+ callbacks: {
+ label: function(context) {
+ return `${context.parsed.x} consultas`;
+ }
+ }
+ }
+ },
+ scales: {
+ x: {
+ beginAtZero: true,
+ grid: {
+ color: '#e9ecef',
+ drawBorder: false,
+ },
+ ticks: {
+ color: '#6c757d',
+ font: {
+ size: 12
+ }
+ }
+ },
+ y: {
+ grid: {
+ display: false,
+ },
+ ticks: {
+ color: '#6c757d',
+ font: {
+ size: 11
+ },
+ maxRotation: 0,
+ // Remover callback que truncava os nomes - mostrar nomes completos
+ }
+ }
+ },
+ animation: {
+ duration: 1000,
+ easing: 'easeInOutQuart'
+ },
+ layout: {
+ padding: {
+ left: 20,
+ right: 30,
+ top: 10,
+ bottom: 10
+ }
+ },
+ elements: {
+ bar: {
+ borderRadius: 4,
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+function AdminDashboard() {
+ const [patients, setPatients] = useState([]);
+ const [doctors, setDoctors] = useState([]);
+ const [consulta, setConsulta] = useState([]);
+ const [countPaciente, setCountPaciente] = useState(0);
+ const [countMedico, setCountMedico] = useState(0);
+ // Estados para os gráficos
+ const [consultasMensaisDataReal, setConsultasMensaisDataReal] = useState([]);
+ const [pacientesStatusDataReal, setPacientesStatusDataReal] = useState([]);
+ const [consultasPorMedicoData, setConsultasPorMedicoData] = useState([]);
+ const [taxaCancelamentosData, setTaxaCancelamentosData] = useState([]);
+ const [appointments, setAppointments] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [currentTime, setCurrentTime] = useState(new Date());
+ const [previewUrl, setPreviewUrl] = useState(AvatarForm);
+
+ const tokenUsuario = getAccessToken();
+ const userId = getUserId();
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const requestOptions = {
+ method: "GET",
+ headers: {
+ apikey:
+ supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ redirect: "follow",
+ };
+
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ setLoading(true);
+
+ // Buscar pacientes
+ const patientsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/patients`,
+ requestOptions
+ );
+ const patientsData = await patientsResponse.json();
+ const patientsArr = Array.isArray(patientsData) ? patientsData : [];
+ setPatients(patientsArr);
+ setConsulta(patientsArr);
+ setCountPaciente(patientsArr.length);
+
+ // Processar status dos pacientes
+ if (patientsArr.length > 0) {
+ const ativos = patientsArr.filter(p => p.active !== false).length;
+ const inativos = patientsArr.length - ativos;
+
+ const statusData = [
+ { name: 'Ativos', value: ativos, color: '#007bff' },
+ { name: 'Inativos', value: inativos, color: '#ffa500' }
+ ];
+
+ setPacientesStatusDataReal(statusData);
+ }
+
+ // Buscar médicos
+ const doctorsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/doctors`,
+ requestOptions
+ );
+ const doctorsData = await doctorsResponse.json();
+ const doctorsArr = Array.isArray(doctorsData) ? doctorsData : [];
+ setDoctors(doctorsArr);
+ setCountMedico(doctorsArr.length);
+
+ // Buscar consultas
+ const appointmentsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/appointments`,
+ requestOptions
+ );
+ const appointmentsData = await appointmentsResponse.json();
+ const appointmentsArr = Array.isArray(appointmentsData) ? appointmentsData : [];
+ setAppointments(appointmentsArr);
+
+ // Processar dados dos gráficos
+ processConsultasMensais(appointmentsArr);
+ await processConsultasPorMedico(appointmentsArr, doctorsArr);
+ processTaxaCancelamentos(appointmentsArr);
+
+
+ } catch (error) {
+
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadData();
+ }, []);
+
+ // useEffect para atualizar o relógio em tempo real
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCurrentTime(new Date());
+ }, 1000); // Atualiza a cada segundo
+
+ return () => clearInterval(timer); // Limpa o timer quando o componente é desmontado
+ }, []);
+
+ // useEffect para carregar avatar do usuário (mesma lógica da navbar)
+ useEffect(() => {
+ const loadAvatar = async () => {
+ if (!userId) return;
+
+ const myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+
+ const requestOptions = {
+ headers: myHeaders,
+ method: 'GET',
+ redirect: 'follow'
+ };
+
+ try {
+ const response = await fetch(`${supabaseUrl}/storage/v1/object/avatars/${userId}/avatar.png`, requestOptions);
+
+ if (response.ok) {
+ const blob = await response.blob();
+ const imageUrl = URL.createObjectURL(blob);
+ setPreviewUrl(imageUrl);
+ return; // Avatar encontrado
+ }
+ } catch (error) {
+
+ }
+
+ // Se chegou até aqui, não encontrou avatar - mantém o padrão
+
+ };
+
+ loadAvatar();
+ }, [userId]);
+
+ // Processar dados das consultas mensais
+ const processConsultasMensais = (appointmentsData) => {
+ const meses = [
+ 'Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun',
+ 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'
+ ];
+
+ const consultasPorMes = meses.map(mes => ({ mes, consultas: 0 }));
+
+ if (appointmentsData && appointmentsData.length > 0) {
+ appointmentsData.forEach(appointment => {
+ if (appointment.scheduled_at) {
+ const data = new Date(appointment.scheduled_at);
+ const mesIndex = data.getMonth();
+ if (mesIndex >= 0 && mesIndex < 12) {
+ consultasPorMes[mesIndex].consultas++;
+ }
+ }
+ });
+ }
+
+
+ setConsultasMensaisDataReal(consultasPorMes);
+ };
+
+ // Processar dados da taxa de cancelamentos
+ const processTaxaCancelamentos = (appointmentsData) => {
+ const meses = [
+ 'Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun',
+ 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'
+ ];
+
+ const cancelamentosPorMes = meses.map(mes => ({
+ mes,
+ realizadas: 0,
+ canceladas: 0,
+ total: 0
+ }));
+
+ if (appointmentsData && appointmentsData.length > 0) {
+
+
+ appointmentsData.forEach(appointment => {
+ if (appointment.scheduled_at) {
+ const data = new Date(appointment.scheduled_at);
+ const mesIndex = data.getMonth();
+ const anoAtual = new Date().getFullYear();
+ const anoConsulta = data.getFullYear();
+
+ // Processar apenas consultas do ano atual
+ if (mesIndex >= 0 && mesIndex < 12 && anoConsulta === anoAtual) {
+ cancelamentosPorMes[mesIndex].total++;
+
+ // Verificar diferentes possíveis campos de status de cancelamento
+ const isCancelled =
+ appointment.status === 'cancelled' ||
+ appointment.status === 'canceled' ||
+ appointment.cancelled === true ||
+ appointment.is_cancelled === true ||
+ appointment.appointment_status === 'cancelled' ||
+ appointment.appointment_status === 'canceled';
+
+ if (isCancelled) {
+ cancelamentosPorMes[mesIndex].canceladas++;
+ } else {
+ cancelamentosPorMes[mesIndex].realizadas++;
+ }
+ }
+ }
+ });
+
+ // Calcular porcentagens e manter valores absolutos para tooltip
+ cancelamentosPorMes.forEach(mes => {
+ if (mes.total > 0) {
+ const realizadasCount = mes.realizadas;
+ const canceladasCount = mes.canceladas;
+
+ mes.realizadas = Math.round((realizadasCount / mes.total) * 100);
+ mes.canceladas = Math.round((canceladasCount / mes.total) * 100);
+
+ // Garantir que soma seja 100%
+ if (mes.realizadas + mes.canceladas !== 100 && mes.total > 0) {
+ mes.realizadas = 100 - mes.canceladas;
+ }
+ } else {
+ // Se não há dados, mostrar 100% realizadas
+ mes.realizadas = 100;
+ mes.canceladas = 0;
+ }
+ });
+
+
+ setTaxaCancelamentosData(cancelamentosPorMes);
+ } else {
+
+ setTaxaCancelamentosData([]);
+ }
+ };
+
+ // Processar dados das consultas por médico
+ const processConsultasPorMedico = async (appointmentsData, doctorsData) => {
+ try {
+
+
+ // Criar mapa de médicos
+ const doctorsMap = {};
+ doctorsData.forEach(doctor => {
+ let doctorName = doctor.full_name || doctor.name || `Médico ${doctor.id}`;
+
+ // Apenas limpar espaços em branco, manter nome completo
+ doctorName = doctorName.trim();
+
+ doctorsMap[doctor.id] = doctorName;
+ });
+
+
+
+ // Contar consultas por médico
+ const consultasPorMedico = {};
+ appointmentsData.forEach(appointment => {
+
+ if (appointment.doctor_id) {
+ const doctorName = doctorsMap[appointment.doctor_id] || `Médico ${appointment.doctor_id}`;
+ consultasPorMedico[doctorName] = (consultasPorMedico[doctorName] || 0) + 1;
+ }
+ });
+
+
+ // Converter para array e ordenar por número de consultas (maior para menor)
+ const chartData = Object.entries(consultasPorMedico)
+ .map(([medico, consultas]) => ({ medico, consultas }))
+ .sort((a, b) => b.consultas - a.consultas)
+ .slice(0, 10); // Mostrar apenas os top 10 médicos
+
+
+ setConsultasPorMedicoData(chartData);
+ } catch (error) {
+ setConsultasPorMedicoData([]);
+ }
+ };
+
+
+
+
+
+
+ return (
+
+
+ {/* Header com informações do admin */}
+
+
+
+
+
+
+
👨💼 Olá, {getFullName()}!
+
É ótimo tê-lo novamente no MediConnect. Acompanhe o desempenho da sua clínica, mantenha o controle de tudo em um só lugar e continue fazendo-a crescer todos os dias!
+
+
+
+ 🕒 {currentTime.toLocaleString('pt-BR')}
+
+
+
+

+
+
+
+
+
+
+
+ {/* Cards de estatísticas */}
+
+
+
+
+
+
+
+
{countPaciente}
+ Total Pacientes
+
+
+
+
+
+
+
+
+
+
+
{countMedico}
+ Total Médicos
+
+
+
+
+
+
+
+
+
+
+
{appointments.length}
+ Total Consultas
+
+
+
+
+
+
+
+
+
+
+
80
+ Atendidos
+
+
+
+
+
+ {/* Seção dos Gráficos */}
+
+ {/* Consultas por Mês */}
+
+
+
+
📊 Consultas por Mês ({new Date().getFullYear()})
+
+
+ {loading ? (
+
+
+
+
Carregando dados...
+
+
+ ) : consultasMensaisDataReal.length > 0 ? (
+
+ ) : (
+
+
+
+
Nenhum dado encontrado
+
+
+ )}
+
+
+
+
+ {/* Top 10 Médicos */}
+
+
+
+
🏆 Top 10 Médicos (Consultas)
+
+
+ {loading ? (
+
+
+
+
Carregando dados...
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ {/* Pacientes Ativos/Inativos */}
+
+
+
+
👥 Pacientes Ativos x Inativos
+
+
+ {loading ? (
+
+
+
+
Carregando dados...
+
+
+ ) : pacientesStatusDataReal.length > 0 ? (
+
+ ) : (
+
+
+
+
Nenhum dado encontrado
+
+
+ )}
+
+
+
+
+ {/* Taxa de Cancelamentos */}
+
+
+
+
📉 Taxa de Cancelamentos
+
+
+ {loading ? (
+
+
+
+
Carregando dados...
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+// CSS customizado para o AdminDashboard (mesmo estilo do PatientDashboard)
+const style = document.createElement('style');
+style.textContent = `
+ .user-info-banner {
+ position: relative;
+ overflow: hidden;
+ }
+
+ .user-info-banner::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml,
');
+ pointer-events: none;
+ }
+
+ .dash-widget {
+ transition: transform 0.2s ease;
+ }
+
+ .dash-widget:hover {
+ transform: translateY(-3px);
+ }
+
+ .card {
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+ }
+
+ .card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15) !important;
+ }
+`;
+
+if (!document.head.querySelector('[data-admin-dashboard-styles]')) {
+ style.setAttribute('data-admin-dashboard-styles', 'true');
+ document.head.appendChild(style);
+}
+
+export default AdminDashboard;
\ No newline at end of file
diff --git a/src/pages/AdminApp/CreateUser.jsx b/src/pages/AdminApp/CreateUser.jsx
new file mode 100644
index 0000000..35cbfd8
--- /dev/null
+++ b/src/pages/AdminApp/CreateUser.jsx
@@ -0,0 +1,711 @@
+import { useEffect, useState } from "react";
+import { getAccessToken } from "../../utils/auth";
+import Swal from 'sweetalert2';
+
+
+function CreateUser() {
+ const tokenUsuario = getAccessToken()
+ const [search, setSearch] = useState("");
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [showModal, setShowModal] = useState(false);
+ const [roleFilter, setRoleFilter] = useState("");
+ const [period, setPeriod] = useState("");
+ const [startDate, setStartDate] = useState("");
+ const [endDate, setEndDate] = useState("");
+ const [formData, setFormData] = useState({
+ full_name: "",
+ email: "",
+ phone: "",
+ cpf: "",
+ role: "secretaria",
+ password: ""
+ });
+ const [submitting, setSubmitting] = useState(false);
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const getHeaders = () => {
+ const token = getAccessToken();
+ return {
+ "apikey": supabaseAK,
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json",
+ };
+ };
+
+ const formatDate = (isoString) => {
+ if (!isoString) return "-";
+ const date = new Date(isoString);
+ return date.toLocaleString("pt-BR", {
+ day: "2-digit",
+ month: "2-digit",
+ year: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+ };
+
+ const fetchUsersAndRoles = async () => {
+ try {
+ const headers = getHeaders();
+
+ // Buscar perfis
+ const resProfiles = await fetch(
+ `${supabaseUrl}/rest/v1/profiles`,
+ { method: "GET", headers }
+ );
+ if (!resProfiles.ok) throw new Error("Erro ao buscar perfis");
+ const profiles = await resProfiles.json();
+
+ // Buscar roles dos usuários
+ const resRoles = await fetch(
+ `${supabaseUrl}/rest/v1/user_roles`,
+ { method: "GET", headers }
+ );
+ if (!resRoles.ok) throw new Error("Erro ao buscar roles");
+ const roles = await resRoles.json();
+
+ // Merge profiles com roles
+ const merged = profiles.map((profile) => {
+ const userRoles = roles.filter((r) => r.user_id === profile.id);
+ const cargos = userRoles.length > 0 ? userRoles.map(r => r.role).join(", ") : "Sem cargo";
+
+ return {
+ ...profile,
+ role: cargos,
+ };
+ });
+
+ setUsers(merged);
+ } catch (err) {
+ console.error("Erro ao carregar usuários e roles:", err);
+ Swal.fire({
+ title: "Erro!",
+ text: "Erro ao carregar usuários",
+ icon: "error",
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchUsersAndRoles();
+ }, []);
+
+ const handleInputChange = (e) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+ };
+
+ const handleCreateUser = async (e) => {
+ e.preventDefault();
+ setSubmitting(true);
+
+ try {
+ // headers
+ const myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+ myHeaders.append("Content-Type", "application/json");
+
+ // validações básicas
+ if (!formData.email || !formData.password || !formData.full_name) {
+ Swal.fire("Campos obrigatórios", "Preencha nome, e-mail e senha.", "warning");
+ setSubmitting(false);
+ return;
+ }
+ if (formData.password.length < 6) {
+ Swal.fire("Senha inválida", "A senha deve ter pelo menos 6 caracteres.", "warning");
+ setSubmitting(false);
+ return;
+ }
+ if (!formData.role || formData.role.trim() === "") {
+ Swal.fire("Cargo obrigatório", "Selecione um cargo para o usuário.", "warning");
+ setSubmitting(false);
+ return;
+ }
+
+ const role = (formData.role || "").toString().trim(); // garante string exata
+
+ // payload 1: role como string (conforme docs)
+ const payload1 = {
+ email: formData.email.trim(),
+ password: formData.password,
+ full_name: formData.full_name.trim(),
+ phone: formData.phone || "",
+ cpf: formData.cpf || "",
+ role: role,
+ ...(role === "paciente" && {
+ create_patient_record: true,
+ phone_mobile: formData.phone || ""
+ })
+ };
+
+ console.log("Tentando criar usuário (payload1):", payload1);
+
+ let response = await fetch(
+ `${supabaseUrl}/functions/v1/create-user-with-password`,
+ {
+ method: "POST",
+ headers: myHeaders,
+ body: JSON.stringify(payload1)
+ }
+ );
+
+ // tenta ler resposta (json se possível, senão texto)
+ let result;
+ try {
+ result = await response.json();
+ } catch (err) {
+ result = await response.text();
+ }
+ console.log("Resposta (payload1):", response.status, result);
+
+ // Se OK, finaliza
+ if (response.ok) {
+ Swal.fire({
+ title: "Sucesso!",
+ html: `
+
+
Usuário criado com sucesso!
+
Nome: ${result.user?.full_name || formData.full_name}
+
Email: ${result.user?.email || formData.email}
+
Cargo: ${role}
+
Telefone: ${formData.phone || "Não informado"}
+
+ `,
+ icon: "success",
+ });
+
+ setShowModal(false);
+ setFormData({ full_name: "", email: "", phone: "", role: "secretaria", password: "" });
+ await fetchUsersAndRoles();
+ return;
+ }
+
+ // Se 400 e menciona role ou resposta indicar role inválida, tenta reenviar usando roles: [role]
+ const errMsg = typeof result === "string" ? result : JSON.stringify(result);
+ const mentionsRole = /role|roles|invalid role|role inválida|role not allowed/i.test(errMsg);
+
+ if ((response.status === 400 || response.status === 422) && mentionsRole) {
+ const payload2 = {
+ email: formData.email.trim(),
+ password: formData.password,
+ full_name: formData.full_name.trim(),
+ phone: formData.phone || "",
+ cpf: formData.cpf || "",
+ roles: [role], // tentativa alternativa
+ ...(role === "paciente" && {
+ create_patient_record: true,
+ phone_mobile: formData.phone || ""
+ })
+ };
+
+ console.log("Servidor rejeitou role. Tentando payload2 (roles array):", payload2);
+
+ const response2 = await fetch(
+ `${supabaseUrl}/functions/v1/create-user-with-password`,
+ {
+ method: "POST",
+ headers: myHeaders,
+ body: JSON.stringify(payload2)
+ }
+ );
+
+ let result2;
+ try {
+ result2 = await response2.json();
+ } catch (err) {
+ result2 = await response2.text();
+ }
+ console.log("Resposta (payload2):", response2.status, result2);
+
+ if (response2.ok) {
+ Swal.fire("Sucesso!", result2.message || "Usuário criado com sucesso!", "success");
+ setShowModal(false);
+ setFormData({ full_name: "", email: "", phone: "", role: "secretaria", password: "" });
+ await fetchUsersAndRoles();
+ return;
+ } else {
+ // falha na segunda tentativa — mostra detalhe
+ const detail = typeof result2 === "string" ? result2 : JSON.stringify(result2);
+ throw new Error(detail || "Erro ao criar usuário (tentativa com roles array falhou)");
+ }
+ }
+
+ // Se não é erro de role ou tentativas falharam, lança o erro original
+ throw new Error(errMsg || "Erro ao criar usuário");
+ } catch (err) {
+ console.error("Erro ao criar usuário:", err);
+ Swal.fire({
+ title: "Erro!",
+ text: err.message || "Falha ao criar usuário",
+ icon: "error",
+ });
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+
+ // Função para definir períodos e limpar datas
+ const handlePeriodChange = (newPeriod) => {
+ // Se clicar no mesmo período, limpa o filtro
+ if (period === newPeriod) {
+ setPeriod("");
+ } else {
+ setPeriod(newPeriod);
+ }
+
+ // Sempre limpa as datas específicas
+ setStartDate("");
+ setEndDate("");
+ };
+
+ const openCreateModal = () => setShowModal(true);
+ const closeModal = () => {
+ setShowModal(false);
+ setFormData({
+ full_name: "",
+ email: "",
+ phone: "",
+ cpf: "",
+ role: "secretaria",
+ password: ""
+ });
+ };
+
+ const filteredUsers = users.filter(p => {
+ if (!p) return false;
+ const nome = (p.full_name || "").toLowerCase();
+ const cpf = (p.cpf || "").toLowerCase();
+ const email = (p.email || "").toLowerCase();
+ const q = search.toLowerCase();
+
+ // Filtro por texto (nome, cpf, email)
+ const matchesText = nome.includes(q) || cpf.includes(q) || email.includes(q);
+
+ // Filtro por cargo
+ const matchesRole = !roleFilter || (p.role || "").toLowerCase().includes(roleFilter.toLowerCase());
+
+ let dateMatch = true;
+ if (p.created_at) {
+ const userDate = new Date(p.created_at);
+ const now = new Date();
+
+ // Filtros por período
+ if (period === "today") {
+ const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+ const tomorrow = new Date(today);
+ tomorrow.setDate(tomorrow.getDate() + 1);
+ dateMatch = userDate >= today && userDate < tomorrow;
+ } else if (period === "week") {
+ const weekStart = new Date(now);
+ weekStart.setDate(now.getDate() - now.getDay());
+ weekStart.setHours(0, 0, 0, 0);
+ dateMatch = userDate >= weekStart;
+ } else if (period === "month") {
+ const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
+ dateMatch = userDate >= monthStart;
+ }
+
+ // Filtros por data específica
+ if (startDate && endDate) {
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+ end.setHours(23, 59, 59, 999); // Inclui o dia inteiro
+ dateMatch = dateMatch && userDate >= start && userDate <= end;
+ } else if (startDate) {
+ const start = new Date(startDate);
+ dateMatch = dateMatch && userDate >= start;
+ } else if (endDate) {
+ const end = new Date(endDate);
+ end.setHours(23, 59, 59, 999);
+ dateMatch = dateMatch && userDate <= end;
+ }
+ }
+
+
+ return matchesText && matchesRole && dateMatch;
+ });
+
+ const [itemsPerPage1, setItemsPerPage1] = useState(15);
+ const [currentPage1, setCurrentPage1] = useState(1);
+ const indexOfLastPatient = currentPage1 * itemsPerPage1;
+ const indexOfFirstPatient = indexOfLastPatient - itemsPerPage1;
+ const currentUsers = filteredUsers.slice(indexOfFirstPatient, indexOfLastPatient);
+ const totalPages1 = Math.ceil(filteredUsers.length / itemsPerPage1);
+
+
+ useEffect(() => {
+ setCurrentPage1(1);
+ }, [search, roleFilter, period, startDate, endDate]);
+
+ if (loading) return
Carregando usuários...
;
+ return (
+
+
+
+
Lista de Usuários
+
+
+ {/* Todos os filtros em uma única linha */}
+
+ {/* Campo de busca */}
+
setSearch(e.target.value)}
+ style={{ minWidth: "300px", maxWidth: "450px", }}
+ />
+
+ {/* Filtro por cargo */}
+
setRoleFilter(e.target.value)}
+ >
+
+
+
+
+
+
+
+
+
+ {/* Filtro De */}
+
+
+ {
+ setStartDate(e.target.value);
+ if (e.target.value) setPeriod("");
+ }}
+ />
+
+
+ {/* Filtro Até */}
+
+
+ {
+ setEndDate(e.target.value);
+ if (e.target.value) setPeriod("");
+ }}
+ />
+
+
+ {/* Botões rápidos */}
+
+
+
+
+
+
+
+
+
+
+
+ | Nome |
+ Email |
+ Telefone |
+ Cargos |
+ User ID |
+ Criado em |
+
+
+
+ {currentUsers.length > 0 ? (
+ currentUsers.map((user) => (
+
+ | {user.full_name || "-"} |
+ {user.email || "-"} |
+ {user.phone || "-"} |
+
+ {(() => {
+ if (!user.role || user.role === "Sem cargo") {
+ return (
+
+
+ Sem cargo
+
+ );
+ }
+
+ const rolesArray = user.role.split(', ').map(r => r.trim());
+ const roleMap = {
+ 'admin': { icon: 'fa fa-shield', label: 'Admin', color: 'status-red' },
+ 'medico': { icon: 'fa fa-stethoscope', label: 'Médico', color: 'status-purple' },
+ 'gestor': { icon: 'fa fa-briefcase', label: 'Gestor', color: 'status-blue' },
+ 'secretaria': { icon: 'fa fa-phone', label: 'Secretaria', color: 'status-orange' },
+ 'paciente': { icon: 'fa fa-user', label: 'Paciente', color: 'status-green' },
+ 'user': { icon: 'fa fa-user-circle', label: 'User', color: 'status-pink' }
+ };
+
+ return (
+
+ {rolesArray.map((role, index) => {
+ const roleInfo = roleMap[role.toLowerCase()] || { icon: 'fa-question-circle', label: role, color: 'status-gray' };
+ return (
+
+
+ {roleInfo.label}
+
+ );
+ })}
+
+ );
+ })()}
+ |
+ {user.id} |
+ {formatDate(user.created_at)} |
+
+ ))
+ ) : (
+
+ | Nenhum usuário encontrado |
+
+ )}
+
+
+
+
+
+ Total encontrados: {filteredUsers.length}
+
+
+ {
+ setItemsPerPage1(Number(e.target.value));
+ setCurrentPage1(1);
+ }}
+ title="Itens por página"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {showModal && (
+
+
+
+
+
Criar Novo Usuário
+
+
+
+
+
+
+ )}
+
+ );
+}
+
+export default CreateUser;
diff --git a/src/pages/AdminApp/Doctorexceçao.jsx b/src/pages/AdminApp/Doctorexceçao.jsx
new file mode 100644
index 0000000..395c475
--- /dev/null
+++ b/src/pages/AdminApp/Doctorexceçao.jsx
@@ -0,0 +1,339 @@
+import React, { useEffect, useMemo, useState } from "react";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import interactionPlugin from "@fullcalendar/interaction";
+import ptBrLocale from "@fullcalendar/core/locales/pt-br";
+import Swal from "sweetalert2";
+import { getAccessToken } from "../../utils/auth.js";
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+const API_ROOT = `${supabaseUrl}/rest/v1`;
+const API_URL = `${API_ROOT}/doctor_exceptions`;
+const API_DOCTORS = `${API_ROOT}/doctors?select=id,full_name`;
+const API_KEY = supabaseAK;
+
+export default function Doctorexceçao() {
+ const token = getAccessToken();
+
+ const [exceptions, setExceptions] = useState([]);
+ const [doctors, setDoctors] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [err, setErr] = useState("");
+
+ // ---------- CONFIGURAÇÕES COMUNS ----------
+ const commonHeaders = {
+ apikey: API_KEY,
+ Authorization: `Bearer ${token}`,
+ };
+
+ // ---------- CARREGAR DADOS ----------
+ const loadExceptions = async () => {
+ try {
+ setLoading(true);
+ setErr("");
+ const res = await fetch(`${API_URL}?select=*`, { headers: commonHeaders });
+ if (!res.ok) throw new Error(await res.text());
+ const data = await res.json();
+ setExceptions(Array.isArray(data) ? data : []);
+ } catch (e) {
+ setErr(e.message || "Erro ao carregar exceções");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const loadDoctors = async () => {
+ try {
+ const res = await fetch(API_DOCTORS, { headers: commonHeaders });
+ if (!res.ok) throw new Error(await res.text());
+ const data = await res.json();
+ setDoctors(Array.isArray(data) ? data : []);
+ } catch {
+ setDoctors([]);
+ }
+ };
+
+ useEffect(() => {
+ loadDoctors();
+ loadExceptions();
+ }, [token]);
+
+ // ---------- CRIAR EXCEÇÃO ----------
+ const createException = async (payload) => {
+ try {
+ const body = {
+ ...payload,
+ created_by: payload.created_by || payload.doctor_id,
+ };
+
+ const res = await fetch(API_URL, {
+ method: "POST",
+ headers: {
+ ...commonHeaders,
+ "Content-Type": "application/json",
+ Prefer: "return=representation",
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) throw new Error(await res.text());
+ await res.json();
+ await loadExceptions();
+ Swal.fire("Sucesso!", "Exceção criada com sucesso.", "success");
+ } catch (e) {
+ Swal.fire("Erro ao criar", e.message || "Falha ao criar exceção", "error");
+ }
+ };
+
+ // ---------- DELETAR EXCEÇÃO ----------
+ const deleteException = async (id) => {
+ const confirm = await Swal.fire({
+ title: "Excluir exceção?",
+ text: "Essa ação não pode ser desfeita.",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonText: "Sim, excluir",
+ cancelButtonText: "Cancelar",
+ });
+ if (!confirm.isConfirmed) return;
+
+ try {
+ const res = await fetch(`${API_URL}?id=eq.${id}`, {
+ method: "DELETE",
+ headers: commonHeaders,
+ });
+ if (!res.ok) throw new Error(await res.text());
+ await loadExceptions();
+ Swal.fire("Removida!", "Exceção excluída com sucesso.", "success");
+ } catch (e) {
+ Swal.fire("Erro ao excluir", e.message || "Falha ao excluir", "error");
+ }
+ };
+
+ // ---------- EVENTOS DO CALENDÁRIO ----------
+ const events = useMemo(() => {
+ return exceptions.map((ex) => {
+ const isBlock = ex.kind === "bloqueio";
+ return {
+ id: ex.id,
+ title: isBlock ? "Bloqueio" : "Liberação",
+ start: ex.date,
+ allDay: true,
+ backgroundColor: isBlock ? "#ef4444" : "#22c55e",
+ borderColor: isBlock ? "#b91c1c" : "#15803d",
+ textColor: "#fff",
+ };
+ });
+ }, [exceptions]);
+
+ // ---------- HANDLERS ----------
+ const handleDateClick = async (info) => {
+ if (!doctors.length) {
+ Swal.fire("Sem médicos", "Cadastre médicos antes de criar exceções.", "info");
+ return;
+ }
+
+ // 1️⃣ Selecionar médico
+ const doctorOptions = doctors.reduce((acc, d) => {
+ acc[d.id] = d.full_name || d.id;
+ return acc;
+ }, {});
+ const s1 = await Swal.fire({
+ title: `Nova exceção — ${info.dateStr}`,
+ input: "select",
+ inputOptions: doctorOptions,
+ inputPlaceholder: "Selecione o médico",
+ showCancelButton: true,
+ confirmButtonText: "Continuar",
+ didOpen: (popup) => {
+ popup.style.position = "fixed";
+ popup.style.top = "230px";
+ }
+ });
+ if (!s1.isConfirmed || !s1.value) return;
+ const doctor_id = s1.value;
+
+ // 2️⃣ Tipo da exceção
+ const s2 = await Swal.fire({
+ title: "Tipo de exceção",
+ input: "select",
+ inputOptions: {
+ bloqueio: "Bloqueio (remover horários)",
+ liberacao: "Liberação (adicionar horários extras)",
+ },
+ inputPlaceholder: "Selecione o tipo",
+ showCancelButton: true,
+ confirmButtonText: "Continuar",
+ didOpen: (popup) => {
+ popup.style.position = "fixed";
+ popup.style.top = "230px";
+ }
+ });
+ if (!s2.isConfirmed || !s2.value) return;
+ const kind = s2.value;
+
+ // 3️⃣ Motivo
+ const form = await Swal.fire({
+ title: "Motivo (opcional)",
+ input: "text",
+ inputPlaceholder: "Ex: Congresso, folga, manutenção...",
+ showCancelButton: true,
+ confirmButtonText: "Criar exceção",
+ didOpen: (popup) => {
+ popup.style.position = "fixed";
+ popup.style.top = "230px";
+ }
+ });
+ if (!form.isConfirmed) return;
+
+ const payload = {
+ doctor_id,
+ created_by: doctor_id,
+ date: info.dateStr,
+ kind,
+ reason: form.value || null,
+ };
+
+ await createException(payload);
+ };
+
+ const handleEventClick = async (info) => {
+ const e = exceptions.find((x) => x.id === info.event.id);
+ if (!e) return;
+ await Swal.fire({
+ title: e.kind === "bloqueio" ? "Bloqueio" : "Liberação",
+ html: `
Médico: ${
+ doctors.find((d) => d.id === e.doctor_id)?.full_name || e.doctor_id
+ }
+
Data: ${e.date}
+
Motivo: ${e.reason || "-"}`,
+ icon: "info",
+ showCancelButton: true,
+ confirmButtonText: "Excluir",
+ cancelButtonText: "Fechar",
+ }).then((r) => {
+ if (r.isConfirmed) deleteException(e.id);
+ });
+ };
+
+ // ---------- UI ----------
+ return (
+
+
+
+
+
Exceções (Bloqueios / Liberações)
+
+ Clique numa data para adicionar exceções por médico
+
+
+
+
+
+ {/* Calendário */}
+
+
+
+ {loading ? (
+
Carregando calendário…
+ ) : err ? (
+
Erro: {err}
+ ) : (
+
+ )}
+
+
+
+
+ {/* Lista de exceções */}
+
+
+
+
+
Lista de Exceções
+ {exceptions.length} registro(s)
+
+
+
+ {loading ? (
+
Carregando lista…
+ ) : err ? (
+
Erro: {err}
+ ) : exceptions.length === 0 ? (
+
Nenhuma exceção encontrada.
+ ) : (
+
+
+
+
+ | Médico |
+ Data |
+ Tipo |
+ Motivo |
+ Ações |
+
+
+
+ {exceptions.map((ex) => (
+
+ |
+ {doctors.find((d) => d.id === ex.doctor_id)?.full_name ||
+ ex.doctor_id}
+ |
+ {ex.date} |
+
+ {ex.kind === "bloqueio" ? (
+ Bloqueio
+ ) : (
+ Liberação
+ )}
+ |
+ {ex.reason || "-"} |
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+
+
+ * Vermelho = Bloqueio,
+ Verde = Liberação.
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/calendar/Calendar.jsx b/src/pages/AdminApp/calendar/Calendar.jsx
similarity index 100%
rename from src/pages/calendar/Calendar.jsx
rename to src/pages/AdminApp/calendar/Calendar.jsx
diff --git a/src/pages/Agendar/AgendaEdit.jsx b/src/pages/Agendar/AgendaEdit.jsx
deleted file mode 100644
index e9a0803..0000000
--- a/src/pages/Agendar/AgendaEdit.jsx
+++ /dev/null
@@ -1,213 +0,0 @@
-import { useState, useEffect } from "react";
-import { withMask } from "use-mask-input";
-import { Link } from "react-router-dom";
-import "../../assets/css/index.css";
-
-function AgendaEdit() {
- const [minDate, setMinDate] = useState("");
-
- useEffect(() => {
- const getToday = () => {
- const today = new Date();
- const offset = today.getTimezoneOffset();
- today.setMinutes(today.getMinutes() - offset);
- return today.toISOString().split("T")[0];
- };
-
- setMinDate(getToday());
- }, []);
-
- return (
-
-
-
-
-
-
Editar consulta
-
- Informações do paciente
-
-
-
-
-
-
-
-
-
-
- );
-}
-export default AgendaEdit;
\ No newline at end of file
diff --git a/src/pages/Agendar/AgendaForm.jsx b/src/pages/Agendar/AgendaForm.jsx
deleted file mode 100644
index 1d365ff..0000000
--- a/src/pages/Agendar/AgendaForm.jsx
+++ /dev/null
@@ -1,213 +0,0 @@
-import { useState, useEffect } from "react";
-import { withMask } from "use-mask-input";
-import { Link } from "react-router-dom";
-import "../../assets/css/index.css";
-
-function AgendaForm() {
- const [minDate, setMinDate] = useState("");
-
- useEffect(() => {
- const getToday = () => {
- const today = new Date();
- const offset = today.getTimezoneOffset();
- today.setMinutes(today.getMinutes() - offset);
- return today.toISOString().split("T")[0];
- };
-
- setMinDate(getToday());
- }, []);
-
- return (
-
-
-
-
-
-
Nova consulta
-
- Informações do paciente
-
-
-
-
-
-
-
-
-
-
- );
-}
-export default AgendaForm;
\ No newline at end of file
diff --git a/src/pages/Agendar/AgendaList.jsx b/src/pages/Agendar/AgendaList.jsx
deleted file mode 100644
index 604af3b..0000000
--- a/src/pages/Agendar/AgendaList.jsx
+++ /dev/null
@@ -1,231 +0,0 @@
-import "../../assets/css/index.css"
-import { Link } from "react-router-dom";
-import { useState, useEffect, useRef, useLayoutEffect } from "react";
-import { createPortal } from "react-dom";
-
-function DropdownPortal({ anchorEl, isOpen, onClose, className, children }) {
- const menuRef = useRef(null);
- const [stylePos, setStylePos] = useState({
- position: "absolute",
- top: 0,
- left: 0,
- visibility: "hidden",
- zIndex: 1000,
- });
-
- // Posiciona o menu após renderar (medir tamanho do menu)
- useLayoutEffect(() => {
- if (!isOpen) return;
- if (!anchorEl || !menuRef.current) return;
-
- const anchorRect = anchorEl.getBoundingClientRect();
- const menuRect = menuRef.current.getBoundingClientRect();
- const scrollY = window.scrollY || window.pageYOffset;
- const scrollX = window.scrollX || window.pageXOffset;
-
- // tenta alinhar à direita do botão (como dropdown-menu-right)
- let left = anchorRect.right + scrollX - menuRect.width;
- let top = anchorRect.bottom + scrollY;
-
- // evita sair da esquerda da tela
- if (left < 0) left = scrollX + 4;
- // se extrapolar bottom, abre para cima
- if (top + menuRect.height > window.innerHeight + scrollY) {
- top = anchorRect.top + scrollY - menuRect.height;
- }
- setStylePos({
- position: "absolute",
- top: `${Math.round(top)}px`,
- left: `${Math.round(left)}px`,
- visibility: "visible",
- zIndex: 1000,
- });
- }, [isOpen, anchorEl, children]);
-
- // fecha ao clicar fora / ao rolar
- useEffect(() => {
- if (!isOpen) return;
- function handleDocClick(e) {
- const menu = menuRef.current;
- if (menu && !menu.contains(e.target) && anchorEl && !anchorEl.contains(e.target)) {
- onClose();
- }
- }
- function handleScroll() {
- onClose();
- }
- document.addEventListener("mousedown", handleDocClick);
- // captura scroll em qualquer elemento (true)
- document.addEventListener("scroll", handleScroll, true);
- return () => {
- document.removeEventListener("mousedown", handleDocClick);
- document.removeEventListener("scroll", handleScroll, true);
- };
- }, [isOpen, onClose, anchorEl]);
-
- if (!isOpen) return null;
- return createPortal(
-
e.stopPropagation()}
- >
- {children}
-
,
- document.body
- );
-}
-
-function AgendaList() {
- const [openDropdown, setOpenDropdown] = useState(null);
- const anchorRefs = useRef({});
-
- return (
-
-
-
-
-
-
Lista de consultas
-
-
-
-
-
- Adicionar consulta
-
-
-
-
-
-
-
-
-
-
- | ID da cosulta |
- Nome do Paciente |
- Idade |
- Nome do médico |
- Especialidade |
- Data da consulta |
- Hora da consulta |
- Status |
- Ação |
-
-
-
-
- | APT0001 |
- João Miguel |
- 18 |
- Davi Andrade |
- Cardiologista |
- 25 Set 2025 |
- 10:00am - 11:00am |
-
-
- Ativo
-
- |
-
-
-
-
- setOpenDropdown(null)}
- className="dropdown-menu dropdown-menu-right show"
- >
- {/* {
- e.stopPropagation();
- setOpenDropdown(null);
- }}
- >
- Ver Detalhes
- */}
-
- {
- e.stopPropagation();
- setOpenDropdown(null);
- }}
- >
- Editar
-
-
-
-
-
-
- |
-
-
-
-
-
-
-
-
- {/* Modal delete */}
-
-
-
-
-

-
Are you sure want to delete this Appointment?
-
-
-
-
-
-
-
-
- );
-}
-
-export default AgendaList;
\ No newline at end of file
diff --git a/src/pages/Doctor/DoctorEdit.jsx b/src/pages/Doctor/DoctorEdit.jsx
deleted file mode 100644
index bd118eb..0000000
--- a/src/pages/Doctor/DoctorEdit.jsx
+++ /dev/null
@@ -1,422 +0,0 @@
-import "../../assets/css/index.css"
-import { withMask } from "use-mask-input";
-import { useState } from "react";
-import supabase from "../../Supabase"
-import { Link } from "react-router-dom";
-import { useParams } from "react-router-dom";
-import { useEffect } from "react";
-
-function EditDoctor() {
- const [doctors, setdoctors] = useState([]);
-
- const {id} = useParams()
- useEffect(() => {
- const fetchDoctors = async () => {
- const {data, error} = await supabase
- .from('Doctor')
- .select('*')
- .eq ('id', id)
- .single()
- if(error){
- console.error("Erro ao buscar pacientes:", error);
- }else{
- setdoctors(data);
- }
- };
- fetchDoctors();
- } , []);
-
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setdoctors((prev) => ({
- ...prev,
- [name]: value
- }));
- }
- const handleEdit = async (e) => {
- const { data, error } = await supabase
- .from("Doctor")
- .update([doctors])
- .eq ('id', id)
- .single()
-
- };
-
- const buscarCep = (e) => {
- const cep = doctors.cep.replace(/\D/g, '');
- console.log(cep);
- fetch(`https://viacep.com.br/ws/${cep}/json/`)
- .then(response => response.json())
- .then(data => {
- console.log(data)
- // salvando os valores para depois colocar nos inputs
- setValuesFromCep(data)
- // estou salvando os valoeres no patientData
- setdoctors((prev) => ({
- ...prev,
- cidade: data.localidade || '',
- estado: data.estado || '',
- logradouro: data.logradouro || "",
- bairro: data.bairro || '',
- }));
- })
- }
- const setValuesFromCep = (data) => {
- document.getElementById('cidade').value = data.localidade || '';
- document.getElementById('estado').value = data.uf || '';
- document.getElementById('logradouro').value= data.logradouro || '';
- document.getElementById('bairro').value= data.bairro || '';
- }
- return (
-
- {/* FORMULÁRIO*/}
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-export default EditDoctor
diff --git a/src/pages/Doctor/DoctorForm.jsx b/src/pages/Doctor/DoctorForm.jsx
deleted file mode 100644
index 70cbbdf..0000000
--- a/src/pages/Doctor/DoctorForm.jsx
+++ /dev/null
@@ -1,448 +0,0 @@
-import "../../assets/css/index.css"
-import { withMask } from "use-mask-input";
-import { useState } from "react";
-import supabase from "../../Supabase"
-import { Link } from "react-router-dom";
-import { useNavigate } from "react-router-dom";
-
-function DoctorForm() {
-
- const [doctorData, setdoctorData] = useState({
- nome: "",
- sobrenome: "",
- cpf: "",
- crm: "",
- senha: "",
- confirmarsenha: "",
- email: "",
- data_nascimento: "",
- telefone: "",
- sexo: "",
- endereco: "",
- numero: "",
- cidade: "",
- estado: "",
- cep: "",
- biografia: "",
- status: "inativo",
- especialidade: "",
- bairro:"",
- referencia:"",
- logradouro:"",
- complemento:""
- });
-
- const handleChange = (e) => {
- const { name, value } = e.target;
- setdoctorData((prev) => ({
- ...prev,
- [name]: value
- }));
- }
- const navigate = useNavigate();
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- const requiredFields = ["nome","cpf","crm","senha","confirmarsenha","data_nascimento","sexo","cep","logradouro","numero","bairro","cidade","estado","especialidade","email","telefone","data_nascimento"];
- const missing = requiredFields.filter(f => !doctorData[f] || doctorData[f].toString().trim() === "");
- if (missing.length > 0) {
- alert("Preencha todos os campos obrigatórios.");
- return;
- }
-
- // Verificar se senha e confirmarSenha são iguais
- if (doctorData.senha !== doctorData.confirmarsenha) {
- alert("Senha e Confirmar Senha não coincidem.");
- return;
- }
- const { data, error } = await supabase
- .from("Doctor")
- .insert([doctorData]);
-
- if (error) {
- console.error("Erro ao cadastrar doutor:", error);
- alert(`Erro ao cadastrar doutor: ${error.message}`);
- } else {
- alert("Doutor cadastrado com sucesso!");
- navigate("/doctorlist");
- }
- };
-
- const buscarCep = (e) => {
- const cep = doctorData.cep.replace(/\D/g, '');
- console.log(cep);
- fetch(`https://viacep.com.br/ws/${cep}/json/`)
- .then(response => response.json())
- .then(data => {
- console.log(data)
- // salvando os valores para depois colocar nos inputs
- setValuesFromCep(data)
- // estou salvando os valoeres no patientData
- setdoctorData((prev) => ({
- ...prev,
- cidade: data.localidade || '',
- estado: data.estado || '',
- logradouro: data.logradouro || "",
- bairro: data.bairro || '',
- }));
- })
- }
- const setValuesFromCep = (data) => {
- document.getElementById('cidade').value = data.localidade || '';
- document.getElementById('estado').value = data.uf || '';
- document.getElementById('logradouro').value= data.logradouro || '';
- document.getElementById('bairro').value= data.bairro || '';
- }
- return (
-
- {/* FORMULÁRIO*/}
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-export default DoctorForm
diff --git a/src/pages/Doctor/DoctorList.jsx b/src/pages/Doctor/DoctorList.jsx
deleted file mode 100644
index 74d3472..0000000
--- a/src/pages/Doctor/DoctorList.jsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import "../../assets/css/index.css";
-import { useState, useEffect } from "react";
-import { Link } from "react-router-dom";
-import supabase from "../../Supabase";
-
-function Doctors() {
- const [doctors, setDoctors] = useState([]);
- const [openDropdown, setOpenDropdown] = useState(null);
-
- useEffect(() => {
- const fetchDoctors = async () => {
- const { data, error } = await supabase
- .from("Doctor")
- .select("*");
- if (error) {
- console.error("Erro ao buscar pacientes:", error);
- } else {
- setDoctors(data);
- }
- };
- fetchDoctors();
- }, []);
-
- const handleDelete = async (id) => {
- if (window.confirm("Tem certeza que deseja excluir este médico?")) {
- const { error } = await supabase.from("Doctor").delete().eq("id", id);
- if (error) console.error("Erro ao deletar médico:", error);
- else setDoctors(doctors.filter((doc) => doc.id !== id));
- }
- };
-
- return (
-
-
-
-
-
Médicos
-
-
-
- Adicionar Médico
-
-
-
-
-
- {doctors.map((doctor) => (
-
-
-
-
-

-
-
-
- {/* Dropdown estilizado */}
-
-
-
- {openDropdown === doctor.id && (
-
- {/* Ver Detalhes */}
- e.stopPropagation()}
- >
- Ver Detalhes
-
- {/* Edit */}
-
- Editar
-
-
- {/* Delete */}
-
-
- )}
-
-
-
-
- {doctor.nome} {doctor.sobrenome}
-
-
-
{doctor.especialidade}
-
- {doctor.cidade}
-
-
-
- ))}
-
-
-
- {/* Modal delete (não alterado) */}
-
-
-
-
-

-
Are you sure want to delete this Doctor?
-
-
-
-
-
-
- );
-}
-
-export default Doctors;
diff --git a/src/pages/Doctor/DoctorProfile.jsx b/src/pages/Doctor/DoctorProfile.jsx
deleted file mode 100644
index c5851b2..0000000
--- a/src/pages/Doctor/DoctorProfile.jsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import "../../assets/css/index.css";
-import supabase from "../../Supabase";
-import { useState } from "react";
-import { useEffect } from "react";
-import { useParams } from "react-router-dom";
-function DoctorProfile() {
- const [doctorData, setdoctorData] = useState([]);
- const {id} = useParams()
- useEffect(() => {
- const fetchDoctors = async () => {
- const {data, error} = await supabase
- .from('Doctor')
- .select('*')
- .eq ('id', id)
- .single()
- if(error){
- console.error("Erro ao buscar pacientes:", error);
- }else{
- setdoctorData(data);
- }
- };
- fetchDoctors();
- } , []);
-
- return (
-
- {/* Page Content */}
-
-
-
-
- {/* Profile Header */}
-
-
-
-
-
-
-
-
-
-
- -
- Phone:
- {doctorData.telefone}
-
- -
- Email:
- {doctorData.email}
-
- -
- Data de nascimento:
- {doctorData.data_nascimento}
-
- -
- Região
- {doctorData.cidade}, {doctorData.estado}, Brasil
-
- -
- Sexo
- {doctorData.sexo}
-
-
-
-
-
-
-
-
-
-
-
- {/* Tabs */}
-
-
-
-
-
-
-
-
-
Biografia
-
-
-
-
-
{doctorData.biografia}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
-export default DoctorProfile
\ No newline at end of file
diff --git a/src/pages/DoctorApp/DoctorApp.jsx b/src/pages/DoctorApp/DoctorApp.jsx
index ce85bdd..d055cba 100644
--- a/src/pages/DoctorApp/DoctorApp.jsx
+++ b/src/pages/DoctorApp/DoctorApp.jsx
@@ -1,100 +1,97 @@
-import { Outlet, NavLink } from "react-router-dom";
-import "../../assets/css/index.css";
+import { Outlet, NavLink, useLocation } from "react-router-dom";
+import './../../assets/css/index.css'
+import Navbar from '../../components/layouts/Navbar'
+import { useState } from "react";
+import Chatbox from '../../components/chat/Chatbox';
+import AccessibilityWidget from '../../components/AccessibilityWidget';
+import { Link } from "react-router-dom";
+import { useResponsive } from '../../utils/useResponsive';
+import { getAccessToken } from '../../utils/auth.js';
+import { getUserRole } from '../../utils/userInfo.js';
+import { Navigate } from 'react-router-dom';
+import Sidebar from "../../components/layouts/Sidebar.jsx";
function DoctorApp() {
+ const [isSidebarOpen, setSidebarOpen] = useState(false);
+ const location = useLocation();
+
+ // 2. Adicione a função para alternar o estado
+ const toggleSidebar = () => {
+ setSidebarOpen(!isSidebarOpen);
+ };
+
+ // 3. Crie a string de classe que será aplicada dinamicamente
+ const mainWrapperClass = isSidebarOpen ? 'main-wrapper sidebar-open' : 'main-wrapper';
+
+ // Função para verificar se a rota está ativa
+ const isActive = (path) => {
+ const currentPath = location.pathname;
+
+ // Verificação exata primeiro
+ if (currentPath === path) return true;
+
+ // Verificação de subrotas (ex: /doctor/patients/edit/123)
+ if (currentPath.startsWith(path + '/')) return true;
+
+ // Verificações específicas para páginas de edição/criação
+ if (path === '/doctor/patients' && (
+ currentPath.includes('/doctor/editpatient/') ||
+ currentPath.includes('/doctor/patientform') ||
+ currentPath.includes('/doctor/patient/')
+ )) return true;
+
+ if (path === '/doctor/prontuariolist' && (
+ currentPath.includes('/doctor/prontuario/') ||
+ currentPath.includes('/doctor/editprontuario/') ||
+ currentPath.includes('/doctor/prontuarioform')
+ )) return true;
+
+ if (path === '/doctor/consultas' && (
+ currentPath.includes('/doctor/consulta/') ||
+ currentPath.includes('/doctor/editconsulta/') ||
+ currentPath.includes('/doctor/consultaform')
+ )) return true;
+
+ if (path === '/doctor/laudolist' && (
+ currentPath.includes('/doctor/laudo/') ||
+ currentPath.includes('/doctor/editlaudo/') ||
+ currentPath.includes('/doctor/laudoform')
+ )) return true;
+
+ return false;
+ };
+ const token = getAccessToken();
+ const user = getUserRole();
+ // Verificação de autenticação
+ if (!token) {
+ return
;
+ }
+
+ // Verificação de role
+ if (user !== 'medico') {
+ return (
+
+
+
+
❌ Acesso Negado
+
Apenas médicos podem acessar esta área.
+
+
+
+
+ );
+ }
return (
-
- {/* Header */}
-
-
- {/* Sidebar */}
-
-
- {/* Conteúdo */}
-
);
}
-export default DoctorApp;
+export default DoctorApp;
\ No newline at end of file
diff --git a/src/pages/DoctorApp/DoctorCalendar.jsx b/src/pages/DoctorApp/DoctorCalendar.jsx
index 87572f0..62ec53f 100644
--- a/src/pages/DoctorApp/DoctorCalendar.jsx
+++ b/src/pages/DoctorApp/DoctorCalendar.jsx
@@ -1,55 +1,256 @@
+// --- SEU JSX COMPLETO ---
+
+import React, { useState, useEffect } from "react";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import ptBrLocale from "@fullcalendar/core/locales/pt-br";
+import "../../assets/css/index.css";
+import { getAccessToken } from '../../utils/auth';
+import { getDoctorId } from "../../utils/userInfo";
-function DoctorCalendar() {
- return (
-
-
Calendário do Médico
-
-
+// Função para formatar data/hora igual ao ConsultaList
+function formatDateTime(dateString) {
+ if (!dateString) return '';
+ try {
+ const [datePart, timePart] = dateString.split('T');
+ const [year, month, day] = datePart.split('-');
+ const [hour, minute] = timePart.split(':');
+ return `${day}/${month}/${year} ${hour}:${minute}`;
+ } catch {
+ return dateString;
+ }
+}
+
+export default function DoctorCalendar() {
+ const [events, setEvents] = useState([]);
+ const [editingEvent, setEditingEvent] = useState(null);
+ const [showPopup, setShowPopup] = useState(false);
+ const [showActionModal, setShowActionModal] = useState(false);
+ const [step, setStep] = useState(1);
+ const [newEvent, setNewEvent] = useState({ title: "", time: "" });
+ const [selectedDate, setSelectedDate] = useState(null);
+ const [selectedEvent, setSelectedEvent] = useState(null);
+ const [pacientesMap, setPacientesMap] = useState({});
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+ const colorsByType = {
+ presencial: "#4dabf7",
+ online: "#f76c6c",
+ Rotina: "#4dabf7",
+ Cardiologia: "#f76c6c",
+ Otorrino: "#f7b84d",
+ Pediatria: "#6cf78b"
+ };
+
+ const doctor_id = getDoctorId();
+ const tokenUsuario = getAccessToken();
+
+ useEffect(() => {
+ const fetchAppointments = async () => {
+ try {
+ const requestOptions = {
+ method: "GET",
+ headers: { apikey: supabaseAK, Authorization: `Bearer ${tokenUsuario}` },
+ };
+
+ const response = await fetch(
+ `${supabaseUrl}/rest/v1/appointments?doctor_id=eq.${doctor_id}`, requestOptions
+ );
+
+ const result = await response.json();
+ const consultas = Array.isArray(result) ? result : [];
+
+ const idsUnicos = [...new Set(consultas.map((c) => c.patient_id))];
+ const promises = idsUnicos.map(async (id) => {
+ try {
+ const res = await fetch(
+ `${supabaseUrl}/rest/v1/patients?id=eq.${id}`,
+ {
+ method: "GET",
+ headers: { apikey: supabaseAK, Authorization: `Bearer ${tokenUsuario}` },
+ }
+ );
+ const data = await res.json();
+ return { id, full_name: data?.[0]?.full_name || "Nome não encontrado" };
+ } catch {
+ return { id, full_name: "Nome não encontrado" };
+ }
+ });
+
+ const pacientes = await Promise.all(promises);
+ const map = {};
+ pacientes.forEach((p) => (map[p.id] = p.full_name));
+ setPacientesMap(map);
+
+ const calendarEvents = consultas.map((consulta) => {
+ const [date, timeFull] = consulta.scheduled_at.split('T');
+ const time = timeFull ? timeFull.substring(0, 5) : '';
+ return {
+ id: consulta.id,
+ title: map[consulta.patient_id] || "Paciente",
+ date: date,
+ time: time,
+ start: `${date}T${time}:00`,
+ type: consulta.appointment_type || "presencial",
+ color: colorsByType[consulta.appointment_type] || "#4dabf7",
+ appointmentData: consulta,
+ };
+ });
+
+ setEvents(calendarEvents);
+ } catch (error) {
+ console.error("Erro ao buscar consultas:", error);
+ }
+ };
+
+ if (doctor_id) fetchAppointments();
+ }, [doctor_id, tokenUsuario]);
+
+ const handleDateClick = (arg) => {
+ setSelectedDate(arg.dateStr);
+ setNewEvent({ title: "", time: "" });
+ setStep(1);
+ setEditingEvent(null);
+ setShowPopup(true);
+ };
+
+ const handleAddEvent = () => {
+ const eventToAdd = {
+ id: Date.now(),
+ title: newEvent.title,
+ time: newEvent.time,
+ date: selectedDate,
+ start: `${selectedDate}T${newEvent.time}:00`,
+ color: colorsByType[newEvent.type] || "#4dabf7"
+ };
+ setEvents((prev) => [...prev, eventToAdd]);
+ setShowPopup(false);
+ };
+
+ const handleEditEvent = () => {
+ setEvents((prevEvents) =>
+ prevEvents.map((ev) =>
+ ev.id.toString() === editingEvent.id.toString()
+ ? {
+ ...ev,
+ title: newEvent.title,
+ time: newEvent.time,
+ start: `${ev.date}T${newEvent.time}:00`,
+ color: colorsByType[newEvent.type] || "#4dabf7"
+ }
+ : ev
+ )
+ );
+ setEditingEvent(null);
+ setShowPopup(false);
+ setShowActionModal(false);
+ };
+
+ const handleNextStep = () => {
+ if (step < 2) setStep(step + 1);
+ else editingEvent ? handleEditEvent() : handleAddEvent();
+ };
+
+ const handleEventClick = (clickInfo) => {
+ setSelectedEvent(clickInfo.event);
+ setShowActionModal(true);
+ };
+
+ const renderEventContent = (eventInfo) => {
+ const bg =
+ eventInfo.event.backgroundColor ||
+ eventInfo.event.extendedProps?.color ||
+ "#4dabf7";
+
+ const appointmentType = eventInfo.event.extendedProps?.type || "presencial";
+ const typeLabel = appointmentType === "presencial" ? "Presencial" : "Online";
+
+ return (
+
+
+ {eventInfo.event.title}
+
+ •
+ {eventInfo.event.extendedProps.time}
+ );
+ };
- {/* CSS inline para centralizar */}
-
+ /* ADICIONADO PARA DEIXAR A PARTE AZUL ESTILIZÁVEL */
+ slotLabelContent={(arg) => {
+ return {
+ html: `
${arg.text}`
+ };
+ }}
+
+ events={events.map((ev) => ({
+ id: ev.id,
+ title: ev.title,
+ start: `${ev.date}T${ev.time}:00`,
+ color: ev.color,
+ extendedProps: {
+ type: ev.type,
+ time: ev.time,
+ color: ev.color,
+ appointmentData: ev.appointmentData
+ }
+ }))}
+
+ eventContent={renderEventContent}
+ eventClick={handleEventClick}
+ dayCellClassNames="calendar-day-cell"
+ />
+
+
+
);
}
-
-export default DoctorCalendar;
-
diff --git a/src/pages/DoctorApp/DoctorDashboard.jsx b/src/pages/DoctorApp/DoctorDashboard.jsx
index 296b3cb..ed9d1fe 100644
--- a/src/pages/DoctorApp/DoctorDashboard.jsx
+++ b/src/pages/DoctorApp/DoctorDashboard.jsx
@@ -1,68 +1,526 @@
-import { PieChart, Pie, Cell, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, Legend } from "recharts";
+import React, { useState, useEffect } from "react";
+import { Link } from "react-router-dom";
+import { getAccessToken } from "../../utils/auth.js";
+import "../../assets/css/index.css";
+import { getFullName, getUserId } from "../../utils/userInfo";
+import { getUserRole } from "../../utils/userInfo";
+const AvatarForm = "/img/AvatarForm.jpg";
+const banner = "/img/banner.png";
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip as ChartTooltip,
+ Legend as ChartLegend,
+} from 'chart.js';
+import { Bar as ChartJSBar } from 'react-chartjs-2';
+import { withTheme } from "@emotion/react";
-const consultsData = [
- { name: "Consultas", value: 45 },
- { name: "Exames", value: 20 },
- { name: "Laudos", value: 15 },
- { name: "Receitas", value: 25 },
-];
-
-const COLORS = ["#0088FE", "#00C49F", "#FFBB28", "#FF8042"];
+// Registrar componentes do Chart.js
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ ChartTooltip,
+ ChartLegend
+);
function DoctorDashboard() {
+ const [patients, setPatients] = useState([]);
+ const [appointments, setAppointments] = useState([]);
+ const [todayAppointments, setTodayAppointments] = useState([]);
+ const [recentConsults, setRecentConsults] = useState([]);
+ const [followUpPatients, setFollowUpPatients] = useState([]);
+ const [alerts, setAlerts] = useState([]);
+ const [draftReports, setDraftReports] = useState([]);
+ // Estados para os gráficos médicos
+
+ const [loading, setLoading] = useState(true);
+ const [currentTime, setCurrentTime] = useState(new Date());
+ const [previewUrl, setPreviewUrl] = useState(AvatarForm);
+
+ const tokenUsuario = getAccessToken();
+ const userId = getUserId();
+ const role = getUserRole();
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const requestOptions = {
+ method: "GET",
+ headers: {
+ apikey: supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ redirect: "follow",
+ };
+
+ useEffect(() => {
+ const loadDoctorData = async () => {
+ try {
+ setLoading(true);
+
+ // Buscar pacientes
+ const patientsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/patients`,
+ requestOptions
+ );
+ const patientsData = await patientsResponse.json();
+ const patientsArr = Array.isArray(patientsData) ? patientsData : [];
+ setPatients(patientsArr);
+
+ // Buscar consultas do médico (filtrar pelo doctor_id se disponível)
+ const appointmentsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/appointments`,
+ requestOptions
+ );
+ const appointmentsData = await appointmentsResponse.json();
+ const appointmentsArr = Array.isArray(appointmentsData) ? appointmentsData : [];
+ setAppointments(appointmentsArr);
+
+ // Buscar laudos em draft
+ const reportsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/reports?status=eq.draft`,
+ requestOptions
+ );
+ const reportsData = await reportsResponse.json();
+ const reportsArr = Array.isArray(reportsData) ? reportsData : [];
+ setDraftReports(reportsArr);
+
+ // Processar dados específicos do médico
+ processTodayAppointments(appointmentsArr, patientsArr);
+ processRecentConsults(appointmentsArr, patientsArr);
+ processFollowUpPatients(appointmentsArr, patientsArr);
+ processConsultasMensais(appointmentsArr);
+ processComparecimentoData(appointmentsArr);
+ processAlerts(appointmentsArr, reportsArr);
+
+ } catch (error) {
+ console.error('Erro ao carregar dados do médico:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Inject custom CSS for DoctorDashboard
+ const styleId = 'doctor-dashboard-styles';
+ if (!document.getElementById(styleId)) {
+ const style = document.createElement('style');
+ style.id = styleId;
+ style.textContent = `
+ [data-dashboard="doctor"] .custom-badge {
+ padding: 4px 12px;
+ border-radius: 20px;
+ font-size: 11px;
+ font-weight: 500;
+ text-transform: uppercase;
+ }
+ [data-dashboard="doctor"] .status-green {
+ background-color: #e8f5e8;
+ color: #2e7d32;
+ border: 1px solid #c8e6c9;
+ }
+ [data-dashboard="doctor"] .status-yellow {
+ background-color: #fff8e1;
+ color: #f57f17;
+ border: 1px solid #ffecb3;
+ }
+ [data-dashboard="doctor"] .status-red {
+ background-color: #ffebee;
+ color: #c62828;
+ border: 1px solid #ffcdd2;
+ }
+ `;
+ document.head.appendChild(style);
+ }
+
+ loadDoctorData();
+ }, []);
+
+ // useEffect para atualizar o relógio em tempo real
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCurrentTime(new Date());
+ }, 1000); // Atualiza a cada segundo
+
+ return () => clearInterval(timer); // Limpa o timer quando o componente é desmontado
+ }, []);
+
+ // useEffect para carregar avatar do usuário (mesma lógica da navbar)
+ useEffect(() => {
+ const loadAvatar = async () => {
+ if (!userId) return;
+
+ const myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+
+ const requestOptions = {
+ headers: myHeaders,
+ method: 'GET',
+ redirect: 'follow'
+ };
+
+ try {
+ const response = await fetch(`${supabaseUrl}/storage/v1/object/avatars/${userId}/avatar.png`, requestOptions);
+
+ if (response.ok) {
+ const blob = await response.blob();
+ const imageUrl = URL.createObjectURL(blob);
+ setPreviewUrl(imageUrl);
+ return; // Avatar encontrado
+ }
+ } catch (error) {
+
+ }
+
+ // Se chegou até aqui, não encontrou avatar - mantém o padrão
+
+ };
+
+ loadAvatar();
+ }, [userId]);
+
+ // Processar agenda do dia
+ const processTodayAppointments = (appointmentsData, patientsData) => {
+ const today = new Date().toISOString().split('T')[0];
+ const todayAppts = appointmentsData.filter(apt => {
+ if (!apt.scheduled_at) return false;
+ const aptDate = apt.scheduled_at.split('T')[0];
+ return aptDate === today;
+ });
+
+ const todayWithPatients = todayAppts.map(apt => {
+ const patient = patientsData.find(p => p.id === apt.patient_id);
+ return {
+ ...apt,
+ patient_name: patient?.name || patient?.full_name || 'Paciente não encontrado',
+ time: apt.scheduled_at ? apt.scheduled_at.split('T')[1].substring(0, 5) : ''
+ };
+ }).sort((a, b) => a.time.localeCompare(b.time));
+
+ setTodayAppointments(todayWithPatients);
+ };
+
+ // Processar consultas recentes
+ const processRecentConsults = (appointmentsData, patientsData) => {
+ const recent = appointmentsData
+ .filter(apt => apt.scheduled_at && new Date(apt.scheduled_at) < new Date())
+ .sort((a, b) => new Date(b.scheduled_at) - new Date(a.scheduled_at))
+ .slice(0, 5)
+ .map(apt => {
+ const patient = patientsData.find(p => p.id === apt.patient_id);
+ return {
+ ...apt,
+ patient_name: patient?.name || patient?.full_name || 'Paciente não encontrado',
+ date: apt.scheduled_at ? new Date(apt.scheduled_at).toLocaleDateString('pt-BR') : ''
+ };
+ });
+
+ setRecentConsults(recent);
+ };
+
+ // Processar pacientes em acompanhamento
+ const processFollowUpPatients = (appointmentsData, patientsData) => {
+ // Selecionar pacientes com consultas recorrentes ou em tratamento
+ const followUp = patientsData.slice(0, 10);
+ setFollowUpPatients(followUp);
+ };
+
+ // Processar dados de consultas mensais
+ const processConsultasMensais = (appointmentsData) => {
+ // Lógica para processar consultas mensais para gráficos
+ // Implementar conforme necessário
+ };
+
+ // Processar dados de comparecimento
+ const processComparecimentoData = (appointmentsData) => {
+ // Lógica para processar dados de comparecimento
+ // Implementar conforme necessário
+ };
+
+ // Processar alertas
+ const processAlerts = (appointmentsData, reportsData = []) => {
+ const alertsArray = [];
+
+ // Verificar laudos em draft
+ if (reportsData.length > 0) {
+ alertsArray.push({
+ message: `${reportsData.length} laudo${reportsData.length > 1 ? 's' : ''} pendente${reportsData.length > 1 ? 's' : ''} de confirmação`,
+ type: 'warning',
+ icon: 'fa-file-text-o',
+ action: 'Confirmar',
+ link: '/doctor/reports'
+ });
+ }
+
+ // Verificar consultas próximas (próximas 2 horas)
+ const now = new Date();
+ const twoHoursLater = new Date(now.getTime() + 2 * 60 * 60 * 1000);
+
+ appointmentsData.forEach(apt => {
+ if (apt.scheduled_at) {
+ const aptDate = new Date(apt.scheduled_at);
+ if (aptDate > now && aptDate <= twoHoursLater) {
+ alertsArray.push({
+ message: `Consulta em ${Math.round((aptDate - now) / (1000 * 60))} minutos`,
+ type: 'warning',
+ icon: 'fa-clock-o',
+ action: 'Ver',
+ link: '/doctor/appointments'
+ });
+ }
+ }
+ });
+
+ // Adicionar outros alertas conforme necessário
+ if (alertsArray.length === 0) {
+ alertsArray.push({
+ message: 'Nenhum alerta no momento',
+ type: 'info',
+ icon: 'fa-info-circle',
+ action: 'OK'
+ });
+ }
+
+ setAlerts(alertsArray);
+ };
return (
-
-
-
-
📊 Dashboard do Médico
-
+
+
+ {/* Header com informações do médico */}
+
- {/* Gráfico de Pizza */}
-
-
Distribuição de Atividades
-
-
-
- {consultsData.map((entry, index) => (
- |
- ))}
-
-
-
-
-
-
- {/* Gráfico de Barras */}
-
-
Consultas por Semana
-
-
-
-
-
-
-
-
-
+
+
+
+
+
👨⚕️ Olá, Dr. {getFullName()}!
+
Hoje é mais um dia para transformar vidas. Revise sua agenda, acompanhe seus pacientes e siga fazendo a diferença com o MediConnect. 💙!
+
+ 🕒 {currentTime.toLocaleString('pt-BR')}
+
+
+
+

+
+
+
+
+ {/* Cards de estatísticas */}
+
+
+
+
+
+
+
+
{todayAppointments.length}
+ Consultas Hoje
+
+
+
+
+
+
+
+
+
+
+
{patients.length}
+ Total Pacientes
+
+
+
+
+
+
+
+
+
+
+
{draftReports.length}
+ Laudos Pendentes
+
+
+
+
+
+
+
+
+
+
+
{alerts.length}
+ Notificações
+
+
+
+
+
+ {/* Ações rápidas */}
+
+
+
+
+
+
+
+
+ Nova Consulta
+
+
+
+
+
+ Meus Pacientes
+
+
+
+
+
+ Laudos
+
+
+
+
+
+ Exceções
+
+
+
+
+
+
+
+
+ {/* Alertas Médicos */}
+ {alerts.length > 0 && (
+
+
+
+
+
🔔 Notificações Importantes
+
+
+ {alerts.map((alert, index) => (
+
+
+ {alert.message}
+
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Seções Específicas do Médico */}
+ {/* Primeira linha - Agenda do Dia e Consultas Recentes */}
+
+
+
+
+ Consultas de Hoje
+
+ {loading ? (
+
+
+
+
Carregando agenda...
+
+
+ ) : todayAppointments.length > 0 ? (
+
+ {todayAppointments.map((apt, index) => (
+
+
+ {apt.time}
+
+
+
{apt.patient_name}
+ {apt.chief_complaint || 'Consulta de rotina'}
+
+
+
+ {apt.status === 'completed' ? 'Realizada' : 'Agendada'}
+
+
+
+ ))}
+
+ ) : (
+
+
+
Nenhuma consulta agendada para hoje
+
+ )}
+
+
+
+
+
+
+ Atendimentos Recentes
+
+ {loading ? (
+
+
+
+
Carregando consultas...
+
+
+ ) : recentConsults.length > 0 ? (
+
+ {recentConsults.map((consult, index) => (
+
+
+ {consult.patient_name.charAt(0)}
+
+
+
{consult.patient_name}
+ {consult.date}
+
+
+
+ ))}
+
+ ) : (
+
+
+
Nenhuma consulta recente
+
+ )}
+
+
+
+
+ {/* Charts Section */}
+
+
+
);
}
-export default DoctorDashboard;
+export default DoctorDashboard;
\ No newline at end of file
diff --git a/src/pages/DoctorApp/DoctorPatientList.jsx b/src/pages/DoctorApp/DoctorPatientList.jsx
deleted file mode 100644
index eadeb81..0000000
--- a/src/pages/DoctorApp/DoctorPatientList.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import React, { useState } from "react";
-
-function PatientList() {
- const [searchTerm, setSearchTerm] = useState("");
-
- const patients = [
- {
- nome: "João Miguel",
- cpf: "091.959.495-69",
- telefone: "+55 (75) 99961-7296",
- email: "Joaomiguel80@gmail.com",
- },
- ];
-
- const filteredPatients = patients.filter(
- (p) =>
- p.nome.toLowerCase().includes(searchTerm.toLowerCase()) ||
- p.cpf.toLowerCase().includes(searchTerm.toLowerCase()) ||
- p.email.toLowerCase().includes(searchTerm.toLowerCase())
- );
-
- return (
-
- {/* Barra de Pesquisa */}
-
setSearchTerm(e.target.value)}
- className="search-bar"
- />
-
- {/* Tabela */}
-
-
Lista de Pacientes
-
-
-
- | Nome |
- CPF |
- Telefone |
- Email |
- Ações |
-
-
-
- {filteredPatients.map((p, idx) => (
-
- | {p.nome} |
- {p.cpf} |
- {p.telefone} |
- {p.email} |
-
-
-
- |
-
- ))}
-
-
-
-
- );
-}
-
-export default PatientList;
diff --git a/src/pages/DoctorApp/Doctorexceçao.jsx b/src/pages/DoctorApp/Doctorexceçao.jsx
new file mode 100644
index 0000000..5f95f01
--- /dev/null
+++ b/src/pages/DoctorApp/Doctorexceçao.jsx
@@ -0,0 +1,356 @@
+import React, { useEffect, useMemo, useState } from "react";
+import FullCalendar from "@fullcalendar/react";
+import dayGridPlugin from "@fullcalendar/daygrid";
+import interactionPlugin from "@fullcalendar/interaction";
+import ptBrLocale from "@fullcalendar/core/locales/pt-br";
+import Swal from "sweetalert2";
+import { getAccessToken } from "../../utils/auth.js";
+// Adicione uma função para pegar o papel do usuário
+import { getUserRole } from '../../utils/userInfo.js';
+
+const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+const API_ROOT = `${supabaseUrl}/rest/v1`;
+const AUTH_URL = `${supabaseUrl}/auth/v1/user`; // Endpoint para pegar dados do user logado
+const API_URL = `${API_ROOT}/doctor_exceptions`;
+const API_DOCTORS = `${API_ROOT}/doctors`;
+const API_KEY = supabaseAK;
+
+export default function Doctorexceçao() {
+ const token = getAccessToken();
+ // Verifica o papel do usuário (admin ou médico)
+ const userRole = getUserRole();
+
+ const [exceptions, setExceptions] = useState([]);
+ const [currentDoctor, setCurrentDoctor] = useState(null); // Estado para o médico logado
+ const [loading, setLoading] = useState(true);
+ const [err, setErr] = useState("");
+
+ // ---------- CONFIGURAÇÕES COMUNS ----------
+ const commonHeaders = {
+ apikey: API_KEY,
+ Authorization: `Bearer ${token}`,
+ };
+
+ // ---------- 1. IDENTIFICAR USUÁRIO LOGADO ----------
+ const loadCurrentUser = async () => {
+ try {
+ setLoading(true);
+ if (userRole === 'admin') {
+ // Se for admin, carrega todas as exceções e define um médico padrão
+ await loadExceptions();
+ setCurrentDoctor({ full_name: 'Administrador' });
+ return;
+ }
+
+ // 1. Pega os dados da autenticação (Auth User)
+ const resAuth = await fetch(AUTH_URL, { headers: commonHeaders });
+ if (!resAuth.ok) throw new Error("Falha ao autenticar usuário");
+ const user = await resAuth.json();
+
+ // 2. Busca o perfil do médico correspondente na tabela 'doctors'
+ // Assumindo que o ID do médico na tabela é igual ao ID do Auth (UUID)
+ const resDoc = await fetch(`${API_DOCTORS}?user_id=eq.${user.id}`, { headers: commonHeaders });
+ if (!resDoc.ok) throw new Error("Perfil de médico não encontrado");
+
+ const docsData = await resDoc.json();
+
+ if (docsData.length > 0) {
+ const doc = docsData[0];
+ setCurrentDoctor(doc);
+ // Só carrega as exceções depois de saber quem é o médico
+ loadExceptions(doc.id);
+ } else {
+ setErr("Seu usuário não está cadastrado como médico.");
+ }
+ } catch (e) {
+ setErr(e.message || "Erro ao carregar perfil do usuário");
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // ---------- 2. CARREGAR DADOS FILTRADOS ----------
+ // Agora recebe o doctorId como argumento para filtrar na API
+ const loadExceptions = async (doctorId) => {
+ try {
+ let url;
+ if (userRole === 'admin') {
+ url = `${API_URL}?select=*,doctor:doctor_id(id,full_name)`; // join para trazer nome do médico
+ } else {
+ url = `${API_URL}?select=*&doctor_id=eq.${doctorId}`;
+ }
+ const res = await fetch(url, { headers: commonHeaders });
+ if (!res.ok) throw new Error(await res.text());
+ const data = await res.json();
+ setExceptions(Array.isArray(data) ? data : []);
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ useEffect(() => {
+ if (token) {
+ loadCurrentUser();
+ }
+ }, [token]);
+
+ // ---------- CRIAR EXCEÇÃO ----------
+ const createException = async (payload) => {
+ try {
+ const body = {
+ ...payload,
+ // Garante que quem cria é o próprio médico
+ created_by: currentDoctor.id,
+ doctor_id: currentDoctor.id
+ };
+
+ const res = await fetch(API_URL, {
+ method: "POST",
+ headers: {
+ ...commonHeaders,
+ "Content-Type": "application/json",
+ Prefer: "return=representation",
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) throw new Error(await res.text());
+ await res.json();
+ // Recarrega usando o ID do médico logado
+ await loadExceptions(currentDoctor.id);
+ Swal.fire("Sucesso!", "Exceção criada com sucesso.", "success");
+ } catch (e) {
+ Swal.fire("Erro ao criar", e.message || "Falha ao criar exceção", "error");
+ }
+ };
+
+ // ---------- DELETAR EXCEÇÃO ----------
+ const deleteException = async (id) => {
+ const confirm = await Swal.fire({
+ title: "Excluir exceção?",
+ text: "Essa ação não pode ser desfeita.",
+ icon: "warning",
+ showCancelButton: true,
+ confirmButtonText: "Sim, excluir",
+ cancelButtonText: "Cancelar",
+ });
+ if (!confirm.isConfirmed) return;
+
+ try {
+ const res = await fetch(`${API_URL}?id=eq.${id}`, {
+ method: "DELETE",
+ headers: commonHeaders,
+ });
+ if (!res.ok) throw new Error(await res.text());
+
+ // Recarrega usando o ID do médico logado
+ await loadExceptions(currentDoctor.id);
+ Swal.fire("Removida!", "Exceção excluída com sucesso.", "success");
+ } catch (e) {
+ Swal.fire("Erro ao excluir", e.message || "Falha ao excluir", "error");
+ }
+ };
+
+ // ---------- EVENTOS DO CALENDÁRIO ----------
+ const events = useMemo(() => {
+ return exceptions.map((ex) => {
+ const isBlock = ex.kind === "bloqueio";
+ return {
+ id: ex.id,
+ title: isBlock ? "Bloqueio" : "Liberação",
+ start: ex.date,
+ allDay: true,
+ backgroundColor: isBlock ? "#ef4444" : "#22c55e",
+ borderColor: isBlock ? "#b91c1c" : "#15803d",
+ textColor: "#fff",
+ };
+ });
+ }, [exceptions]);
+
+ // ---------- HANDLERS ----------
+ const handleDateClick = async (info) => {
+ if (!currentDoctor) {
+ Swal.fire("Erro", "Perfil de médico não identificado.", "error");
+ return;
+ }
+
+ // REMOVIDA A ETAPA 1 (Seleção de médico)
+ // Agora vai direto para a seleção do tipo de exceção
+
+ // 1️⃣ Tipo da exceção
+ const s2 = await Swal.fire({
+ title: `Nova exceção — ${info.dateStr}`,
+ text: "O que deseja fazer nesta data?",
+ input: "select",
+ inputOptions: {
+ bloqueio: "Bloqueio (Não atender)",
+ liberacao: "Liberação (Atender extra)",
+ },
+ inputPlaceholder: "Selecione o tipo",
+ showCancelButton: true,
+ confirmButtonText: "Continuar",
+ didOpen: (popup) => {
+ popup.style.position = "fixed";
+ popup.style.top = "230px";
+ }
+ });
+ if (!s2.isConfirmed || !s2.value) return;
+ const kind = s2.value;
+
+ // 2️⃣ Motivo
+ const form = await Swal.fire({
+ title: "Motivo (opcional)",
+ input: "text",
+ inputPlaceholder: "Ex: Congresso, folga, manutenção...",
+ showCancelButton: true,
+ confirmButtonText: "Criar exceção",
+ didOpen: (popup) => {
+ popup.style.position = "fixed";
+ popup.style.top = "230px";
+ }
+ });
+ if (!form.isConfirmed) return;
+
+ const payload = {
+ // O ID vem do estado global do componente
+ doctor_id: currentDoctor.id,
+ date: info.dateStr,
+ kind,
+ reason: form.value || null,
+ };
+
+ await createException(payload);
+ };
+
+ const handleEventClick = async (info) => {
+ const e = exceptions.find((x) => x.id === info.event.id);
+ if (!e) return;
+
+ await Swal.fire({
+ title: e.kind === "bloqueio" ? "Bloqueio" : "Liberação",
+ html: `
Data: ${e.date}
+
Motivo: ${e.reason || "-"}`,
+ icon: "info",
+ showCancelButton: true,
+ confirmButtonText: "Excluir",
+ cancelButtonText: "Fechar",
+ }).then((r) => {
+ if (r.isConfirmed) deleteException(e.id);
+ });
+ };
+
+ // ---------- UI ----------
+ return (
+
+
+
+
+
+
+ Minhas Exceções {currentDoctor ? `(${currentDoctor.full_name})` : ""}
+
+
+ Clique numa data para bloquear ou liberar sua agenda
+
+
+
+
+
+ {/* Calendário */}
+
+
+
+ {loading ? (
+
Identificando médico e carregando agenda...
+ ) : err ? (
+
{err}
+ ) : (
+
+ )}
+
+
+
+
+ {/* Lista de exceções */}
+
+
+
+
+
Minhas Exceções Registradas
+ {exceptions.length} registro(s)
+
+
+
+ {loading ? (
+
Carregando...
+ ) : !err && exceptions.length === 0 ? (
+
Nenhuma exceção encontrada para você.
+ ) : (
+
+
+
+
+ | Data |
+ Tipo |
+ Motivo |
+ {userRole === 'admin' && Médico | }
+ Ações |
+
+
+
+ {exceptions.map((ex) => (
+
+ | {ex.date} |
+
+ {ex.kind === "bloqueio" ? (
+ Bloqueio
+ ) : (
+ Liberação
+ )}
+ |
+ {ex.reason || "-"} |
+ {userRole === 'admin' && {ex.doctor?.full_name || '-'} | }
+
+
+ |
+
+ ))}
+
+
+
+ )}
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/DoctorApp/Prontuario/DoctorProntuario.jsx b/src/pages/DoctorApp/Prontuario/DoctorProntuario.jsx
new file mode 100644
index 0000000..1a39075
--- /dev/null
+++ b/src/pages/DoctorApp/Prontuario/DoctorProntuario.jsx
@@ -0,0 +1,790 @@
+// src/pages/DoctorApp/DoctorProntuario.jsx
+import React, { useState, useEffect } from "react";
+import { useParams, useNavigate, useLocation } from "react-router-dom";
+
+
+function DoctorProntuario() {
+ const { id } = useParams();
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ // Receber paciente via state da navegação ou buscar por ID
+ const [paciente, setPaciente] = useState(location.state?.paciente || null);
+ const [prontuario, setProntuario] = useState("");
+ const [historico, setHistorico] = useState([]);
+ const [retorno, setRetorno] = useState("3 meses");
+ const [peso, setPeso] = useState("");
+ const [altura, setAltura] = useState("");
+ const [imc, setImc] = useState("");
+ const [pressaoArterial, setPressaoArterial] = useState("");
+ const [temperatura, setTemperatura] = useState("");
+ const [observacoes, setObservacoes] = useState("");
+
+ // Estados para anexos
+ const [anexos, setAnexos] = useState([]);
+ const [arquivoSelecionado, setArquivoSelecionado] = useState(null);
+
+ // Estados para o cronômetro
+ const [atendimentoIniciado, setAtendimentoIniciado] = useState(false);
+ const [atendimentoPausado, setAtendimentoPausado] = useState(false);
+ const [atendimentoFinalizado, setAtendimentoFinalizado] = useState(false);
+ const [tempoDecorrido, setTempoDecorrido] = useState(0);
+ const [cronometroAtivo, setCronometroAtivo] = useState(false);
+
+ // Dados mockados dos pacientes (igual ao da lista)
+ const pacientesMock = [
+ {
+ id: 1,
+ nome: "João Silva Santos",
+ cpf: "123.456.789-00",
+ data_nascimento: "15/03/1985",
+ telefone: "(11) 99999-9999",
+ email: "joao.silva@email.com",
+ status: "ativo",
+ endereco: "Rua das Flores, 123 - São Paulo/SP",
+ idade: "39 anos",
+ primeiraConsulta: "15/01/2024",
+ convenio: "Unimed",
+ atendimentos: 3,
+ faltas: 0
+ },
+ {
+ id: 2,
+ nome: "Maria Oliveira Costa",
+ cpf: "987.654.321-00",
+ data_nascimento: "22/07/1990",
+ telefone: "(11) 88888-8888",
+ email: "maria.oliveira@email.com",
+ status: "ativo",
+ endereco: "Av. Paulista, 1000 - São Paulo/SP",
+ idade: "33 anos",
+ primeiraConsulta: "20/02/2024",
+ convenio: "Amil",
+ atendimentos: 2,
+ faltas: 1
+ },
+ {
+ id: 3,
+ nome: "Pedro Almeida Souza",
+ cpf: "456.789.123-00",
+ data_nascimento: "10/12/1978",
+ telefone: "(11) 77777-7777",
+ email: "pedro.almeida@email.com",
+ status: "inativo",
+ endereco: "Rua Augusta, 500 - São Paulo/SP",
+ idade: "45 anos",
+ primeiraConsulta: "05/03/2024",
+ convenio: "Bradesco Saúde",
+ atendimentos: 1,
+ faltas: 0
+ },
+ {
+ id: 4,
+ nome: "Ana Pereira Lima",
+ cpf: "789.123.456-00",
+ data_nascimento: "05/09/1995",
+ telefone: "(11) 66666-6666",
+ email: "ana.pereira@email.com",
+ status: "ativo",
+ endereco: "Rua Consolação, 200 - São Paulo/SP",
+ idade: "28 anos",
+ primeiraConsulta: "10/04/2024",
+ convenio: "SulAmérica",
+ atendimentos: 4,
+ faltas: 0
+ },
+ {
+ id: 5,
+ nome: "Carlos Rodrigues Ferreira",
+ cpf: "321.654.987-00",
+ data_nascimento: "30/01/1982",
+ telefone: "(11) 55555-5555",
+ email: "carlos.rodrigues@email.com",
+ status: "arquivado",
+ endereco: "Alameda Santos, 800 - São Paulo/SP",
+ idade: "42 anos",
+ primeiraConsulta: "25/05/2024",
+ convenio: "NotreDame Intermédica",
+ atendimentos: 2,
+ faltas: 1
+ }
+ ];
+
+ // Histórico médico mockado específico para cada paciente
+ const historicoPorPaciente = {
+ 1: [
+ {
+ id: 1,
+ data: "15/01/2024",
+ tipo: "Consulta Inicial",
+ diagnostico: "Paciente em bom estado geral. Realizado check-up preventivo.",
+ medico: "Dr. José Rodrigues",
+ duracao: "45 minutos",
+ retorno: "6 meses",
+ dadosAntropometricos: {
+ peso: "78.5 kg",
+ altura: "175 cm",
+ imc: "25.6",
+ pressaoArterial: "120/80 mmHg",
+ temperatura: "36.5°C"
+ },
+ observacoes: "Paciente sem queixas. Exames dentro da normalidade.",
+ anexos: [
+ { id: 1, nome: "raio-x-torax.pdf", tipo: "pdf", tamanho: "2.4 MB" }
+ ]
+ }
+ ],
+ 2: [
+ {
+ id: 1,
+ data: "20/02/2024",
+ tipo: "Consulta de Rotina",
+ diagnostico: "Controle de pressão arterial.",
+ medico: "Dr. José Rodrigues",
+ duracao: "30 minutos",
+ retorno: "3 meses",
+ dadosAntropometricos: {
+ peso: "65.2 kg",
+ altura: "165 cm",
+ imc: "23.9",
+ pressaoArterial: "118/78 mmHg",
+ temperatura: "36.7°C"
+ },
+ observacoes: "Paciente com pressão controlada.",
+ anexos: []
+ }
+ ],
+ // Adicione históricos para os outros IDs...
+ };
+
+ useEffect(() => {
+ // Se o paciente foi passado via state, usa ele
+ if (location.state?.paciente) {
+ setPaciente(location.state.paciente);
+ const historicoPaciente = historicoPorPaciente[location.state.paciente.id] || [];
+ setHistorico(historicoPaciente);
+ }
+ // Se não, busca pelo ID na URL
+ else if (id) {
+ const pacienteEncontrado = pacientesMock.find(p => p.id === parseInt(id));
+ setPaciente(pacienteEncontrado);
+ const historicoPaciente = historicoPorPaciente[parseInt(id)] || [];
+ setHistorico(historicoPaciente);
+ }
+ }, [id, location.state]);
+
+ // Resto do código do cronômetro permanece igual...
+ useEffect(() => {
+ let intervalo = null;
+
+ if (cronometroAtivo && !atendimentoPausado && !atendimentoFinalizado) {
+ intervalo = setInterval(() => {
+ setTempoDecorrido(tempo => tempo + 1);
+ }, 1000);
+ } else {
+ clearInterval(intervalo);
+ }
+
+ return () => clearInterval(intervalo);
+ }, [cronometroAtivo, atendimentoPausado, atendimentoFinalizado]);
+
+ const formatarTempo = (segundos) => {
+ const minutos = Math.floor(segundos / 60);
+ const segs = segundos % 60;
+ return `${minutos.toString().padStart(2, '0')}:${segs.toString().padStart(2, '0')}`;
+ };
+
+ const formatarTempoExtenso = (segundos) => {
+ const horas = Math.floor(segundos / 3600);
+ const minutos = Math.floor((segundos % 3600) / 60);
+ const segs = segundos % 60;
+
+ if (horas > 0) {
+ return `${horas}h ${minutos}m ${segs}s`;
+ }
+ return `${minutos}m ${segs}s`;
+ };
+
+ const iniciarAtendimento = () => {
+ setAtendimentoIniciado(true);
+ setAtendimentoPausado(false);
+ setAtendimentoFinalizado(false);
+ setCronometroAtivo(true);
+ setTempoDecorrido(0);
+ };
+
+ const pausarAtendimento = () => {
+ setAtendimentoPausado(true);
+ setCronometroAtivo(false);
+ };
+
+ const retomarAtendimento = () => {
+ setAtendimentoPausado(false);
+ setCronometroAtivo(true);
+ };
+
+ const finalizarAtendimento = () => {
+ const confirmacao = window.confirm(
+ `Tem certeza que deseja finalizar o atendimento?\n\n` +
+ `Paciente: ${paciente.nome}\n` +
+ `Duração: ${formatarTempoExtenso(tempoDecorrido)}\n\n` +
+ `Após finalizar, o tempo será salvo e não poderá ser alterado.`
+ );
+
+ if (confirmacao) {
+ setAtendimentoFinalizado(true);
+ setCronometroAtivo(false);
+ setAtendimentoIniciado(false);
+ setAtendimentoPausado(false);
+ }
+ };
+
+ const calcularIMC = () => {
+ if (peso && altura) {
+ const alturaMetros = parseInt(altura) / 100;
+ const imcCalculado = (parseFloat(peso) / (alturaMetros * alturaMetros)).toFixed(1);
+ setImc(imcCalculado);
+ }
+ };
+
+ useEffect(() => {
+ calcularIMC();
+ }, [peso, altura]);
+
+ const handleArquivoSelecionado = (event) => {
+ const file = event.target.files[0];
+ if (file) {
+ setArquivoSelecionado(file);
+ }
+ };
+
+ const adicionarAnexo = () => {
+ if (arquivoSelecionado) {
+ const novoAnexo = {
+ id: anexos.length + 1,
+ nome: arquivoSelecionado.name,
+ tipo: arquivoSelecionado.type,
+ tamanho: (arquivoSelecionado.size / 1024 / 1024).toFixed(1) + " MB",
+ arquivo: arquivoSelecionado,
+ data: new Date().toLocaleString('pt-BR')
+ };
+
+ setAnexos([...anexos, novoAnexo]);
+ setArquivoSelecionado(null);
+ document.getElementById('fileInput').value = '';
+ }
+ };
+
+ const removerAnexo = (id) => {
+ setAnexos(anexos.filter(anexo => anexo.id !== id));
+ };
+
+ const handleSalvarProntuario = () => {
+ if (prontuario.trim()) {
+ const duracaoFormatada = atendimentoFinalizado ? formatarTempoExtenso(tempoDecorrido) : "Não registrado";
+
+ const dadosAntropometricosText = `
+DADOS ANTROPOMÉTRICOS:
+- Peso: ${peso || "Não informado"}
+- Altura: ${altura || "Não informado"}
+- IMC: ${imc || "Não calculado"}
+- Pressão Arterial: ${pressaoArterial || "Não informada"}
+- Temperatura: ${temperatura || "Não informada"}
+`;
+
+ const observacoesText = observacoes ? `\nOBSERVAÇÕES:\n${observacoes}` : "";
+
+ const textoCompleto = `${dadosAntropometricosText}${observacoesText}\n\nDIAGNÓSTICO E CONDUTA:\n${prontuario}`;
+
+ const novoRegistro = {
+ id: historico.length + 1,
+ data: new Date().toLocaleDateString('pt-BR'),
+ tipo: "Consulta de Rotina",
+ diagnostico: textoCompleto,
+ medico: "Dr. Médico Atual",
+ duracao: duracaoFormatada,
+ retorno: retorno,
+ dadosAntropometricos: {
+ peso: peso || "Não informado",
+ altura: altura || "Não informado",
+ imc: imc || "Não calculado",
+ pressaoArterial: pressaoArterial || "Não informada",
+ temperatura: temperatura || "Não informada"
+ },
+ observacoes: observacoes,
+ anexos: [...anexos]
+ };
+
+ setHistorico([novoRegistro, ...historico]);
+
+ // Limpar formulário
+ setProntuario("");
+ setPeso("");
+ setAltura("");
+ setImc("");
+ setPressaoArterial("");
+ setTemperatura("");
+ setObservacoes("");
+ setAnexos([]);
+ setAtendimentoIniciado(false);
+ setAtendimentoPausado(false);
+ setAtendimentoFinalizado(false);
+ setCronometroAtivo(false);
+ setTempoDecorrido(0);
+
+ alert(`Prontuário salvo com sucesso! Duração da consulta: ${duracaoFormatada}`);
+ }
+ };
+
+ const visualizarCadastro = () => {
+ if (paciente) {
+ alert(`Cadastro Completo de ${paciente.nome}\n\nCPF: ${paciente.cpf}\nTelefone: ${paciente.telefone}\nEmail: ${paciente.email}\nEndereço: ${paciente.endereco}\nData Nasc.: ${paciente.data_nascimento}\nConvênio: ${paciente.convenio || "Não informado"}\nStatus: ${paciente.status}`);
+ }
+ };
+
+ const getIconePorTipo = (tipo) => {
+ if (tipo.includes('image')) return '🖼️';
+ if (tipo.includes('pdf')) return '📄';
+ if (tipo.includes('word')) return '📝';
+ return '📎';
+ };
+
+ if (!paciente) {
+ return (
+
+
Paciente não encontrado
+
+
+ );
+ }
+
+ return (
+
+ {/* Header com informações do paciente específico */}
+
+
+
+
+
Prontuário Médico
+ Paciente ID: {paciente.id} • {paciente.nome}
+
+
+
+
+
+
+ {/* Controle do atendimento */}
+ {!atendimentoIniciado && !atendimentoFinalizado ? (
+
+ ) : atendimentoPausado ? (
+
+ ) : atendimentoIniciado ? (
+
+ ) : null}
+
+ {(atendimentoIniciado || atendimentoPausado) && (
+
+ )}
+
+
+
+ {/* Cronômetro */}
+ {(atendimentoIniciado || atendimentoPausado || atendimentoFinalizado) && (
+
+
+
+ {atendimentoFinalizado ? '✅ Atendimento Finalizado' :
+ atendimentoPausado ? '⏸️ Atendimento Pausado' : '▶️ Atendimento em Andamento'}
+
+ {atendimentoPausado && (
+ (Tempo pausado)
+ )}
+
+
+
+ ⏱️ {formatarTempo(tempoDecorrido)}
+ {atendimentoFinalizado && ` (${formatarTempoExtenso(tempoDecorrido)})`}
+
+
+
+ )}
+
+ {/* Resumo do Paciente ESPECÍFICO */}
+
+
+
Resumo do Paciente
+
+ {paciente.status}
+
+
+
+
+
{paciente.nome}
+
+ ID: {paciente.id}
+ CPF: {paciente.cpf}
+
+
+
+
+
+
+
IDADE
+
{paciente.idade || "N/A"}
+
Nasc: {paciente.data_nascimento}
+
+
+
+
+
+
PRIMEIRA CONSULTA
+
{paciente.primeiraConsulta || "N/A"}
+
Convênio: {paciente.convenio || "N/A"}
+
+
+
+
+
+
ATENDIMENTOS
+
{paciente.atendimentos || 0}
+
realizados
+
+
+
+
+
+
FALTAS
+
{paciente.faltas || 0}
+
registradas
+
+
+
+
+
+
+ {/* Resto do código do prontuário permanece igual... */}
+
+ {/* Coluna 1: Dados da Consulta */}
+
+
+
+
⏱️ Duração da Consulta
+
+
+ {atendimentoFinalizado ? (
+
+
+ {formatarTempo(tempoDecorrido)}
+
+
Tempo final: {formatarTempoExtenso(tempoDecorrido)}
+
+ ) : atendimentoIniciado ? (
+
+
+ {formatarTempo(tempoDecorrido)}
+
+
+ {atendimentoPausado ? 'Tempo pausado' : 'Tempo decorrido'}
+
+
+ ) : (
+
+
+ 00:00
+
+
Inicie o atendimento
+
+ )}
+
+
+
+
+
+
📊 Dados Antropométricos
+
+
+
+
+
+ {/* Coluna 2: Prontuário Principal */}
+
+
+
+
📝 Prontuário
+
+
+
+
+ Dados incluídos automaticamente:
+ • Peso: {peso || "Não informado"}
+ • Altura: {altura || "Não informado"}
+ • IMC: {imc || "Não calculado"}
+ • Pressão: {pressaoArterial || "Não informada"}
+ • Temperatura: {temperatura || "Não informada"}
+ {observacoes && `• Observações: ${observacoes}`}
+
+
+
+
+
+
+
+ {/* Coluna 3: Anexos e Retorno */}
+
+
+
+
📎 Anexos
+
+
+
+
+ PDF, imagens, documentos (máx. 10MB)
+
+
+ {arquivoSelecionado && (
+
+
+ Arquivo selecionado: {arquivoSelecionado.name}
+ ({(arquivoSelecionado.size / 1024 / 1024).toFixed(1)} MB)
+
+
+
+ )}
+
+ {anexos.length > 0 && (
+
+ {anexos.map(anexo => (
+
+
+
+ {getIconePorTipo(anexo.tipo)}
+ {anexo.nome}
+
+
{anexo.tamanho} • {anexo.data}
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
+
📅 Retorno
+
+
+ setRetorno(e.target.value)}
+ className="form-select"
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
📋 Histórico Recente
+ {historico.length}
+
+
+
+ {historico.length === 0 ? (
+
+ Nenhum registro
+
+ ) : (
+
+ {historico.slice(0, 5).map((registro) => (
+
+
+ {registro.data}
+ {registro.duracao}
+
+
{registro.diagnostico.split('\n')[0]}
+
+ {registro.tipo}
+ Ret: {registro.retorno}
+
+ {registro.anexos && registro.anexos.length > 0 && (
+
+
+ 📎 {registro.anexos.length} anexo(s)
+
+
+ )}
+
+ ))}
+
+ )}
+
+
+
+
+
+ );
+}
+
+export default DoctorProntuario;
\ No newline at end of file
diff --git a/src/pages/DoctorApp/Prontuario/DoctorProntuarioList.jsx b/src/pages/DoctorApp/Prontuario/DoctorProntuarioList.jsx
new file mode 100644
index 0000000..1620d2d
--- /dev/null
+++ b/src/pages/DoctorApp/Prontuario/DoctorProntuarioList.jsx
@@ -0,0 +1,309 @@
+// src/pages/DoctorApp/Patient/DoctorPatientList.jsx
+import { Link } from "react-router-dom";
+import React, { useState, useEffect, useRef, useLayoutEffect } from "react";
+import { createPortal } from "react-dom";
+import DoctorProntuario from "./DoctorProntuario";
+import { getAccessToken } from "../../../utils/auth.js";
+
+
+// Componente DropdownPortal (mantido igual)
+function DropdownPortal({ anchorEl, isOpen, onClose, className, children }) {
+ const menuRef = useRef(null);
+ const [stylePos, setStylePos] = useState({
+ position: "absolute",
+ top: 0,
+ left: 0,
+ visibility: "hidden",
+ zIndex: 1000,
+ });
+
+ useLayoutEffect(() => {
+ if (!isOpen) return;
+ if (!anchorEl || !menuRef.current) return;
+
+ const anchorRect = anchorEl.getBoundingClientRect();
+ const menuRect = menuRef.current.getBoundingClientRect();
+ const scrollY = window.scrollY || window.pageYOffset;
+ const scrollX = window.scrollX || window.pageXOffset;
+
+ let left = anchorRect.right + scrollX - menuRect.width;
+ let top = anchorRect.bottom + scrollY;
+
+ if (left < 0) left = scrollX + 4;
+ if (top + menuRect.height > window.innerHeight + scrollY) {
+ top = anchorRect.top + scrollY - menuRect.height;
+ }
+ setStylePos({
+ position: "absolute",
+ top: `${Math.round(top)}px`,
+ left: `${Math.round(left)}px`,
+ visibility: "visible",
+ zIndex: 1000,
+ });
+ }, [isOpen, anchorEl, children]);
+
+ useEffect(() => {
+ if (!isOpen) return;
+ function handleDocClick(e) {
+ const menu = menuRef.current;
+ if (menu && !menu.contains(e.target) && anchorEl && !anchorEl.contains(e.target)) {
+ onClose();
+ }
+ }
+ function handleScroll() {
+ onClose();
+ }
+ document.addEventListener("mousedown", handleDocClick);
+ document.addEventListener("scroll", handleScroll, true);
+ return () => {
+ document.removeEventListener("mousedown", handleDocClick);
+ document.removeEventListener("scroll", handleScroll, true);
+ };
+ }, [isOpen, onClose, anchorEl]);
+
+ if (!isOpen) return null;
+ return createPortal(
+
e.stopPropagation()}
+ >
+ {children}
+
,
+ document.body
+ );
+}
+
+function DoctorPatientList() {
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const [search, setSearch] = useState("");
+ const [patients, setPatients] = useState([]);
+ const [openDropdown, setOpenDropdown] = useState(null);
+ const anchorRefs = useRef({});
+ const tokenUsuario = getAccessToken()
+ var myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+ var requestOptions = {
+ method: 'GET',
+ headers: myHeaders,
+ redirect: 'follow'
+ };
+ useEffect(() => {
+ fetch(`${supabaseUrl}/rest/v1/patients`, requestOptions)
+ .then(response => response.json())
+ .then(result => setPatients(Array.isArray(result) ? result : []))
+ .catch(error => console.log('error', error));
+ }, [])
+
+
+
+ const handleDelete = async (id) => {
+ const confirmDel = window.confirm("Tem certeza que deseja excluir este paciente?");
+ if (!confirmDel) return;
+
+ // Remove localmente (sem API)
+ setPatients((prev) => prev.filter((p) => p.id !== id));
+ setOpenDropdown(null);
+ };
+
+ const filteredPatients = patients.filter(p => {
+ if (!p) return false;
+ const nome = (p.full_name || "").toLowerCase();
+ const cpf = (p.cpf || "").toLowerCase();
+ const email = (p.email || "").toLowerCase();
+ const q = search.toLowerCase();
+ return nome.includes(q) || cpf.includes(q) || email.includes(q);
+ });
+
+ const [itemsPerPage1] = useState(10);
+ const [currentPage1, setCurrentPage1] = useState(1);
+ const indexOfLastPatient = currentPage1 * itemsPerPage1;
+ const indexOfFirstPatient = indexOfLastPatient - itemsPerPage1;
+ const currentPatients = filteredPatients.slice(indexOfFirstPatient, indexOfLastPatient);
+ const totalPages1 = Math.ceil(filteredPatients.length / itemsPerPage1);
+ useEffect(() => {
+ setCurrentPage1(1);
+ }, [search]);
+
+ const mascararCPF = (cpf = "") => {
+ if (cpf.length < 5) return cpf;
+ const inicio = cpf.slice(0, 3);
+ const fim = cpf.slice(-2);
+ return `${inicio}.***.***-${fim}`;
+ };
+
+ return (
+
+
+
+
Prontuários
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+
+
+
+
+
+ | Nome |
+ Cpf |
+ Data de Nascimento |
+ Telefone |
+ Email |
+ Status |
+ Ações |
+
+
+
+ {currentPatients.length > 0 ? (
+ currentPatients.map((p) => (
+
+ | {p.full_name} |
+ {mascararCPF(p.cpf)} |
+ {p.birth_date} |
+ {p.phone_mobile} |
+ {p.email} |
+
+
+ {p.status}
+
+ |
+
+
+
+ {/* BOTÃO DE PRONTUÁRIO - FUNCIONANDO COM DADOS MOCKADOS */}
+
+ Prontuário
+
+
+ {/* Menu de três pontinhos */}
+
+
+ setOpenDropdown(null)}
+ className="dropdown-menu dropdown-menu-right show"
+ >
+ {
+ e.stopPropagation();
+ setOpenDropdown(null);
+ }}
+ >
+ Editar
+
+
+
+
+
+ |
+
+ ))
+ ) : (
+
+ |
+ Nenhum paciente encontrado
+ |
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+export default DoctorPatientList;
\ No newline at end of file
diff --git a/src/pages/LandingPage/HospitalLanding.jsx b/src/pages/LandingPage/HospitalLanding.jsx
new file mode 100644
index 0000000..0aaa77c
--- /dev/null
+++ b/src/pages/LandingPage/HospitalLanding.jsx
@@ -0,0 +1,199 @@
+import React from "react";
+import "../../assets/css/hospital.css";
+import { useEffect } from "react";
+
+export default function HospitalLanding() {
+ const especialidades = [
+ { nome: "Cardiologia", img: "/img/specialities-04.png" },
+ { nome: "Neurologia", img: "/img/specialities-02.png" },
+ { nome: "Ortopedia", img: "/img/specialities-03.png" },
+ { nome: "Odontologia", img: "/img/specialities-05.png" },
+ { nome: "Urologia", img: "/img/specialities-01.png" },
+ ];
+ useEffect(() => {
+ const handleScroll = () => {
+ const header = document.querySelector(".landing-header");
+ if (window.scrollY > 50) {
+ header.classList.add("scrolled");
+ } else {
+ header.classList.remove("scrolled");
+ }
+ };
+ window.addEventListener("scroll", handleScroll);
+ return () => window.removeEventListener("scroll", handleScroll);
+ }, []);
+ return (
+
+ {/* HEADER */}
+
+
+ {/* HERO - NOVO COM IMAGEM DE FUNDO */}
+
+
+
+
Cuidamos de Você com Excelência e Tecnologia
+
+ Atendimento humanizado e especializado — sua saúde é nossa
+ prioridade.
+
+
+ Acessar Sistema
+
+
+
+
+
+ {/* SOBRE */}
+
+
+
+

+
+
+
Sobre Nós
+
+ Oferecendo atendimento médico de excelência,
+ com estrutura moderna e uma equipe comprometida com a vida. No
+ Medi-Connect, cada detalhe é pensado para
+ oferecer segurança, conforto e confiança.
+
+
+
🏥 Estrutura moderna
+
👨⚕️ Profissionais qualificados
+
🕓 Atendimento 24h
+
+
+
+
+
+ {/* ESPECIALIDADES */}
+
+
+
+
Nossas Especialidades
+
+ Conheça as áreas que fazem do Medi-Connect um centro de referência.
+
+
+
+
+ {especialidades.map((esp, i) => (
+
+

+
{esp.nome}
+
+ ))}
+
+
+
+
+ {/* MÉDICO GIGANTE */}
+
+
+
+

+
+
+
Profissionais Dedicados ao Seu Bem-Estar
+
+ Nossa equipe médica é formada por especialistas experientes e
+ comprometidos em oferecer um atendimento humano, empático e de
+ alta qualidade. Aqui, o cuidado vai muito além do tratamento — é
+ sobre confiança, respeito e dedicação a cada paciente.
+
+
+ Fale Conosco
+
+
+
+
+
+ {/* CONTATO */}
+
+
+
+
Entre em Contato
+
+ Estamos prontos para atender você. Tire suas dúvidas, agende uma consulta ou fale com nossa equipe de atendimento.
+
+
+
+
+
+
+
Telefone
+
(11) 4002-8922
+
+
+
+
+
+
Email
+
contato@mediconnect.com
+
+
+
+
+
+
Localização
+
Av. Paulista, 1000 - São Paulo, SP
+
+
+
+
+
+
+
+
+
+ {/* FOOTER */}
+
+
+
+ );
+}
diff --git a/src/pages/Login/Acessounico.jsx b/src/pages/Login/Acessounico.jsx
new file mode 100644
index 0000000..c7fbdb5
--- /dev/null
+++ b/src/pages/Login/Acessounico.jsx
@@ -0,0 +1,147 @@
+import { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import { getAccessToken } from "../../utils/auth.js";
+import { useResponsive } from '../../utils/useResponsive';
+
+export default function MagicLink() {
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const navigate = useNavigate();
+ const [email, setEmail] = useState("");
+ const [emailError, setEmailError] = useState('');
+ const [isTouched, setIsTouched] = useState(false);
+ const [errorMessage, setErrorMessage] = useState('');
+ const [serverError, setServerError] = useState('');
+ const [serverSucsess, setServerSucsess] = useState('');
+ const tokenUsuario =getAccessToken()
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ const emailValidation = validateEmail(email);
+
+ if (emailValidation) {
+ // Se houver erros locais, para a execução antes do fetch
+ return;
+ }
+
+ try {
+ const myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+ myHeaders.append("Content-Type", "application/json");
+ var raw = JSON.stringify({
+ email: email,
+ options: {
+ emailRedirectTo: "https://mediconnect-neon.vercel.app/"
+ }
+ });
+
+ var requestOptions = {
+ method: 'POST',
+ headers: myHeaders,
+ body: raw,
+ redirect: 'follow'
+ };
+ const response = await fetch(
+ `${supabaseUrl}/auth/v1/otp`, requestOptions
+ );
+
+ const result = await response.json();
+ console.log("🔗 Retorno da API de acesso único:", result);
+
+ serverSucsess("Se o e-mail estiver cadastrado, enviamos um link de acesso!");
+ setEmail("");
+
+ } catch (error) {
+ console.error("❌ Erro ao enviar magic link:", error);
+ serverError("Erro ao enviar o link de acesso. Tente novamente.");
+ }
+ };
+
+ const validateEmail = (emailValue) => {
+ let error = '';
+
+ if (emailValue.trim() === '') {
+ error = 'O e-mail não pode ficar vazio.';
+ } else if (!emailValue.includes('@') || !emailValue.includes('.')) {
+ error = 'O e-mail deve conter o símbolo "@" e um ponto (".") seguido por uma extensão.';
+ }
+
+ // Atualiza o estado de erro específico para o email
+ setEmailError(error);
+ return error;
+};
+
+ const handleEmailChange = (e) => {
+ const newValue = e.target.value;
+ setEmail(newValue);
+ if (isTouched) {
+ validateEmail(newValue); // Valida em tempo real
+ }
+
+ const { name, value } = e.target;
+ setConta((prev) => ({
+ ...prev,
+ [name]: value
+ }));
+};
+
+ const handleEmailBlur = (e) => {
+ setIsTouched(true);
+ validateEmail(e.target.value); // Valida ao perder o foco
+};
+
+ return (
+
+
+
+
+
+
+
Você mais próximo de seu médico
+
Consultas online e acompanhamento em tempo real.
+
+
+
+
+
+
Agende sem sair de casa
+
O seu atendimento, na medida da sua agenda.
+
+
+
+
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/pages/Login/Login.jsx b/src/pages/Login/Login.jsx
new file mode 100644
index 0000000..d7884fb
--- /dev/null
+++ b/src/pages/Login/Login.jsx
@@ -0,0 +1,234 @@
+import { useState, useRef } from "react";
+import { useNavigate } from "react-router-dom";
+import ReCAPTCHA from "react-google-recaptcha";
+import { setUserId, setUserEmail, setUserRole, setDoctorId, setPatientId, setFullName } from "../../utils/userInfo";
+import "../../assets/css/login.css";
+
+export default function Login() {
+ const [email, setEmail] = useState('');
+ const [emailError, setEmailError] = useState('');
+ const [password, setPassword] = useState('');
+ const [passwordError, setPasswordError] = useState('');
+ const [isTouched, setIsTouched] = useState(false);
+ const [serverError, setServerError] = useState('');
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const navigate = useNavigate();
+ const [conta, setConta] = useState({ email: "", password: "" });
+ const [showPassword, setShowPassword] = useState(false);
+
+ const recaptchaRef = useRef();
+
+ const togglePasswordVisibility = () => setShowPassword(prev => !prev);
+
+ const validateEmail = (emailValue) => {
+ let error = '';
+ if (emailValue.trim() === '') error = 'O e-mail não pode ficar vazio.';
+ else if (!emailValue.includes('@') || !emailValue.includes('.')) error = 'O e-mail deve conter "@" e um ponto.';
+ setEmailError(error);
+ return error;
+ };
+
+ const validatePassword = (passwordValue) => {
+ let error = '';
+ const MIN_LENGTH = 3;
+ if (passwordValue.trim() === '') error = 'A senha não pode ficar vazia.';
+ else if (passwordValue.length < MIN_LENGTH) error = `A senha deve ter pelo menos ${MIN_LENGTH} caracteres.`;
+ setPasswordError(error);
+ return error;
+ };
+
+ const handleEmailChange = (e) => {
+ const { name, value } = e.target;
+ setConta(prev => ({ ...prev, [name]: value }));
+ setEmail(value);
+ if (isTouched) validateEmail(value);
+ };
+
+ const handleEmailBlur = (e) => {
+ setIsTouched(true);
+ validateEmail(e.target.value);
+ };
+
+ const handlePasswordChange = (e) => {
+ const { name, value } = e.target;
+ setConta(prev => ({ ...prev, [name]: value }));
+ setPassword(value);
+ if (isTouched) validatePassword(value);
+ };
+
+ const handlePasswordBlur = (e) => {
+ setIsTouched(true);
+ validatePassword(e.target.value);
+ };
+
+ const handleLogin = async (e) => {
+ e.preventDefault();
+
+ const recaptchaValue = recaptchaRef.current.getValue();
+ if (!recaptchaValue) {
+ setServerError("Por favor, confirme que você não é um robô.");
+ return;
+ }
+
+ setServerError('');
+ setEmailError('');
+ setPasswordError('');
+
+ if (validateEmail(conta.email) || validatePassword(conta.password)) return;
+
+ try {
+ const loginResp = await fetch(`${supabaseUrl}/auth/v1/token?grant_type=password`, {
+ method: "POST",
+ headers: { "apikey": supabaseAK, "Content-Type": "application/json" },
+ body: JSON.stringify({ email: conta.email, password: conta.password, grant_type: "password" }),
+ redirect: "follow"
+ });
+ const loginResult = await loginResp.json();
+
+ if (!loginResult.access_token) {
+ setServerError("Credenciais inválidas. Verifique seu e-mail e senha.");
+ return;
+ }
+
+ localStorage.setItem("access_token", loginResult.access_token);
+ localStorage.setItem("refresh_token", loginResult.refresh_token);
+
+ const userInfoRes = await fetch(`${supabaseUrl}/functions/v1/user-info`, {
+ method: "GET",
+ headers: { "Authorization": `Bearer ${loginResult.access_token}`, "apikey": supabaseAK, "Content-Type": "application/json" },
+ redirect: "follow"
+ });
+ const userInfo = await userInfoRes.json();
+
+ const userData = {
+ id: userInfo.profile?.id,
+ email: userInfo.user?.email,
+ role: userInfo.roles || [],
+ doctor_id: userInfo.profile?.doctor_id || userInfo.doctor_id || null,
+ patient_id: userInfo.profile?.patient_id || userInfo.patient_id || null,
+ full_name: userInfo.profile?.full_name || userInfo.user?.user_metadata?.full_name || userInfo.user?.email?.split('@')[0] || null
+ };
+
+ if (userData.id) {
+ setUserId(userData.id);
+ setUserEmail(userData.email);
+ if (userData.doctor_id) setDoctorId(userData.doctor_id);
+ if (userData.patient_id) setPatientId(userData.patient_id);
+ if (userData.full_name) setFullName(userData.full_name);
+ }
+
+ const rolePriority = [
+ { role: "admin", path: "/admin/dashboard" },
+ { role: "secretaria", path: "/secretaria/" },
+ { role: "medico", path: "/medico/dashboard" },
+ { role: "user", path: "/patientapp" },
+ { role: "paciente", path: "/paciente" },
+ ];
+
+ const matchedRole = rolePriority.find(r => userData.role.includes(r.role));
+ if (matchedRole) {
+ setUserRole(matchedRole.role);
+ navigate(matchedRole.path);
+ } else {
+ setServerError("Usuário sem função atribuída. Contate o administrador.");
+ }
+
+ } catch (error) {
+ setServerError("Erro ao conectar ao servidor. Tente novamente.");
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
Você mais próximo de seu médico
+
Consultas online e acompanhamento em tempo real.
+
+
+
+
+
+
Agende sem sair de casa
+
O seu atendimento, na medida da sua agenda.
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/pages/Patient/PatientList.jsx b/src/pages/Patient/PatientList.jsx
deleted file mode 100644
index 561c30d..0000000
--- a/src/pages/Patient/PatientList.jsx
+++ /dev/null
@@ -1,264 +0,0 @@
-// PatientList.jsx
-import { Link } from "react-router-dom";
-import "../../assets/css/index.css";
-import React, { useState, useEffect, useRef, useLayoutEffect } from "react";
-import { createPortal } from "react-dom";
-import supabase from "../../Supabase"; // se for usar supabase para delete, senão pode remover
-
-// Componente que renderiza o menu em um portal (document.body) e posiciona em relação ao botão
-function DropdownPortal({ anchorEl, isOpen, onClose, className, children }) {
- const menuRef = useRef(null);
- const [stylePos, setStylePos] = useState({
- position: "absolute",
- top: 0,
- left: 0,
- visibility: "hidden",
- zIndex: 1000,
- });
-
- // Posiciona o menu após renderar (medir tamanho do menu)
- useLayoutEffect(() => {
- if (!isOpen) return;
- if (!anchorEl || !menuRef.current) return;
-
- const anchorRect = anchorEl.getBoundingClientRect();
- const menuRect = menuRef.current.getBoundingClientRect();
- const scrollY = window.scrollY || window.pageYOffset;
- const scrollX = window.scrollX || window.pageXOffset;
-
- // tenta alinhar à direita do botão (como dropdown-menu-right)
- let left = anchorRect.right + scrollX - menuRect.width;
- let top = anchorRect.bottom + scrollY;
-
- // evita sair da esquerda da tela
- if (left < 0) left = scrollX + 4;
- // se extrapolar bottom, abre para cima
- if (top + menuRect.height > window.innerHeight + scrollY) {
- top = anchorRect.top + scrollY - menuRect.height;
- }
- setStylePos({
- position: "absolute",
- top: `${Math.round(top)}px`,
- left: `${Math.round(left)}px`,
- visibility: "visible",
- zIndex: 1000,
- });
- }, [isOpen, anchorEl, children]);
-
- // fecha ao clicar fora / ao rolar
- useEffect(() => {
- if (!isOpen) return;
- function handleDocClick(e) {
- const menu = menuRef.current;
- if (menu && !menu.contains(e.target) && anchorEl && !anchorEl.contains(e.target)) {
- onClose();
- }
- }
- function handleScroll() {
- onClose();
- }
- document.addEventListener("mousedown", handleDocClick);
- // captura scroll em qualquer elemento (true)
- document.addEventListener("scroll", handleScroll, true);
- return () => {
- document.removeEventListener("mousedown", handleDocClick);
- document.removeEventListener("scroll", handleScroll, true);
- };
- }, [isOpen, onClose, anchorEl]);
-
- if (!isOpen) return null;
- return createPortal(
-
e.stopPropagation()}
- >
- {children}
-
,
- document.body
- );
-}
-
-function PatientList() {
- const [search, setSearch] = useState("");
- const [patients, setPatients] = useState([]);
- const [openDropdown, setOpenDropdown] = useState(null);
- const anchorRefs = useRef({}); // guarda referência do botão de cada linha
-
- var requestOptions = {
- method: "GET",
- redirect: "follow",
- };
-
- useEffect(() => {
- fetch("https://mock.apidog.com/m1/1053378-0-default/pacientes", requestOptions)
- .then((response) => response.json())
- .then((result) => {
- console.log("API result:", result);
- setPatients(result.data || []);
- })
- .catch((error) => console.log("error", error));
- }, []);
-
- // Exemplo simples de delete local (confirmação + remove do state)
- const handleDelete = async (id) => {
- const confirmDel = window.confirm("Tem certeza que deseja excluir este paciente?");
- if (!confirmDel) return;
-
- const requestOptions = {
- method: 'DELETE',
- redirect: 'follow'
- };
-
- fetch("https://mock.apidog.com/m1/1053378-0-default/pacientes/", requestOptions)
- .then(response => response.text())
- .then(result => console.log(result))
- .catch(error => console.log('error', error));
-
- // Se quiser apagar no supabase, faça a chamada aqui.
- // const { error } = await supabase.from("Patient").delete().eq("id", id);
- // if (error) { console.error(error); return; }
-
- setPatients((prev) => prev.filter((p) => p.id !== id));
- setOpenDropdown(null);
- };
-
- const filteredPatients = patients.filter((p) => {
- if (!p) return false;
- const nome = (p.nome || "").toLowerCase();
- const cpf = (p.cpf || "").toLowerCase();
- const email = (p.email || "").toLowerCase();
- const q = search.toLowerCase();
- return nome.includes(q) || cpf.includes(q) || email.includes(q);
- });
-
- const mascararCPF = (cpf = "") => {
- if (cpf.length < 5) return cpf;
- const inicio = cpf.slice(0, 3);
- const fim = cpf.slice(-2);
- return `${inicio}.***.***-${fim}`;
- };
-
- return (
-
-
-
-
-
-
Lista de Pacientes
- setSearch(e.target.value)}
- />
-
-
-
-
- Adicionar Paciente
-
-
-
-
-
-
-
-
-
-
- | Nome |
- Cpf |
- Data de Nascimento |
- Telefone |
- Email |
- Status |
- Ações |
-
-
-
- {filteredPatients.length > 0 ? (
- filteredPatients.map((p) => (
-
- | {p.nome} |
- {mascararCPF(p.cpf)} |
- {p.data_nascimento} |
- {p.telefone} |
- {p.email} |
- {p.status} |
-
-
-
-
- setOpenDropdown(null)}
- className="dropdown-menu dropdown-menu-right show"
- >
- {/* {
- e.stopPropagation();
- setOpenDropdown(null);
- }}
- >
- Ver Detalhes
- */}
-
- {
- e.stopPropagation();
- setOpenDropdown(null);
- }}
- >
- Editar
-
-
-
-
-
- |
-
- ))
- ) : (
-
- |
- Nenhum paciente encontrado
- |
-
- )}
-
-
-
-
-
-
-
-
- );
-}
-
-export default PatientList;
-;
\ No newline at end of file
diff --git a/src/pages/PatientApp/AgendarConsultas.jsx b/src/pages/PatientApp/AgendarConsultas.jsx
new file mode 100644
index 0000000..5b76490
--- /dev/null
+++ b/src/pages/PatientApp/AgendarConsultas.jsx
@@ -0,0 +1,561 @@
+import React, { useState, useEffect } from 'react';
+import {
+ Container,
+ Grid,
+ Card,
+ CardContent,
+ Typography,
+ Button,
+ Avatar,
+ Box,
+ Chip,
+ CircularProgress,
+ Alert,
+ Paper,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ Stepper,
+ Step,
+ StepLabel,
+ Checkbox,
+ FormControlLabel
+} from '@mui/material';
+import {
+ ArrowBack,
+ CalendarToday,
+ AccessTime,
+ Person,
+ LocalHospital,
+ CheckCircle,
+ Email,
+ Sms
+} from '@mui/icons-material';
+import { useParams, useNavigate } from 'react-router-dom';
+import { Link } from "react-router-dom";
+import Swal from "sweetalert2";
+import { getAccessToken } from "../../utils/auth.js";
+import { getPatientId } from "../../utils/userInfo";
+import { getUserRole } from '../../utils/userInfo';
+
+const AgendarConsulta = () => {
+ const { medicoId } = useParams();
+ const navigate = useNavigate();
+ const [medico, setMedico] = useState(null);
+ const [horariosDisponiveis, setHorariosDisponiveis] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [dataSelecionada, setDataSelecionada] = useState('');
+ const [horarioSelecionado, setHorarioSelecionado] = useState(null);
+ const [modalConfirmacao, setModalConfirmacao] = useState(false);
+ const [agendando, setAgendando] = useState(false);
+ const [activeStep, setActiveStep] = useState(0);
+ const [enviarEmail, setEnviarEmail] = useState(true);
+ const [enviarSMS, setEnviarSMS] = useState(true);
+ const [minDate, setMinDate] = useState("");
+ const [carregandoHorarios, setCarregandoHorarios] = useState(false);
+ const [formData, setFormData] = useState({
+ scheduled_date: "",
+ scheduled_time: "",
+ chief_complaint: "",
+ patient_notes: ""
+ });
+ let [confirmationModal, setConfirmationModal] = useState(false);
+ const role = getUserRole();
+ const tokenUsuario = getAccessToken();
+ const patientId = getPatientId();
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const headers = {
+ "Content-Type": "application/json",
+ apikey: supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ };
+
+ const handleConfirmationModal = async () => {
+ if (!dataSelecionada || !horarioSelecionado) {
+ alert("Selecione uma data e horário válidos");
+ return;
+ }
+
+ const confirm = window.confirm(`
+ Confirmar agendamento:
+
+ Médico: Dr. ${medico?.nome}
+ Especialidade: ${medico?.especialidade}
+ Data: ${new Date(dataSelecionada).toLocaleDateString('pt-BR')}
+ Horário: ${horarioSelecionado ? horarioSelecionado.datetime.split("T")[1].substring(0, 5) : ''}
+ Valor: R$ ${medico?.valorConsulta}
+
+ Deseja confirmar?
+ `);
+
+ if (confirm) {
+ await confirmarAgendamento();
+
+ alert(`Consulta marcada com sucesso! Sua consulta com Dr. ${medico.nome} foi agendada.`);
+ navigate(`/${role}/consultalist`);
+ }
+ };
+
+ useEffect(() => {
+ const getToday = () => {
+ const today = new Date();
+ const offset = today.getTimezoneOffset();
+ today.setMinutes(today.getMinutes() - offset);
+ return today.toISOString().split("T")[0];
+ };
+
+ setMinDate(getToday());
+ }, []);
+ useEffect(() => {
+ carregarMedicoEHorarios();
+ }, [medicoId]);
+
+ const carregarMedicoEHorarios = async () => {
+ setLoading(true);
+ try {
+ // Buscar dados do médico
+ const medicoResponse = await fetch(
+ `${supabaseUrl}/rest/v1/doctors?id=eq.${medicoId}`,
+ { headers }
+ );
+
+ if (medicoResponse.ok) {
+ const medicoData = await medicoResponse.json();
+ if (medicoData.length > 0) {
+ const doctorData = medicoData[0];
+ setMedico({
+ id: doctorData.id,
+ nome: doctorData.full_name,
+ especialidade: doctorData.specialty,
+ valorConsulta: 250, // Valor fixo por enquanto
+ foto: '',
+ biografia: doctorData.bio || 'Especialista em ' + doctorData.specialty
+ });
+ } else {
+ throw new Error('Médico não encontrado');
+ }
+ } else {
+ throw new Error('Erro ao carregar dados do médico');
+ }
+
+ setLoading(false);
+ } catch (error) {
+ console.error('Erro ao carregar dados:', error);
+ setMedico(medicoMock);
+ setLoading(false);
+ }
+ };
+
+ // Função para buscar horários disponíveis
+ const fetchHorariosDisponiveis = async (date) => {
+ if (!medicoId || !date) {
+ setHorariosDisponiveis([]);
+ return;
+ }
+
+ setCarregandoHorarios(true);
+
+ const startDate = `${date}T00:00:00.000Z`;
+ const endDate = `${date}T23:59:59.999Z`;
+
+ const payload = {
+ doctor_id: medicoId,
+ start_date: startDate,
+ end_date: endDate,
+ appointment_type: "presencial",
+ };
+
+ try {
+ const response = await fetch(
+ `${supabaseUrl}/functions/v1/get-available-slots`,
+ {
+ method: "POST",
+ headers,
+ body: JSON.stringify(payload),
+ }
+ );
+
+ const data = await response.json();
+
+ console.log("🔍 AgendarConsultas - Resposta da Edge Function:", data);
+
+ if (!response.ok) throw new Error(data.error || "Erro ao buscar horários");
+
+ // Usar exatamente o mesmo formato do AgendaForm
+ const slotsDisponiveis = (data?.slots || []).filter((s) => s.available);
+
+ console.log("✅ Slots disponíveis após filtro:", slotsDisponiveis);
+ console.log("🔍 Todos os slots (antes do filtro):", data?.slots);
+ console.log("❌ Slots NÃO disponíveis:", (data?.slots || []).filter((s) => !s.available));
+
+ console.log("✅ AgendarConsultas - Slots disponíveis após filtro:", slotsDisponiveis);
+
+ setHorariosDisponiveis(slotsDisponiveis);
+
+ if (slotsDisponiveis.length === 0) {
+ alert("Nenhum horário disponível para este dia.");
+ }
+ } catch (error) {
+ console.error("Erro ao buscar horários disponíveis:", error);
+ setHorariosDisponiveis([]);
+ alert("Não foi possível obter os horários disponíveis.");
+ } finally {
+ setCarregandoHorarios(false);
+ }
+ };
+
+ // Atualizar horários quando a data muda
+ useEffect(() => {
+ if (dataSelecionada && medicoId) {
+ fetchHorariosDisponiveis(dataSelecionada);
+ }
+ }, [dataSelecionada, medicoId]);
+
+ const selecionarHorario = (horario) => {
+ setHorarioSelecionado(horario);
+ setModalConfirmacao(true);
+ setActiveStep(0);
+ };
+
+ const confirmarAgendamento = async () => {
+ setAgendando(true);
+
+ try {
+ if (!horarioSelecionado || !horarioSelecionado.datetime) {
+ throw new Error("Horário não selecionado corretamente");
+ }
+
+ // Usar exatamente o mesmo formato que o AgendaForm
+ const scheduled_at = horarioSelecionado.datetime;
+
+ const payload = {
+ patient_id: patientId,
+ doctor_id: medicoId,
+ scheduled_at,
+ duration_minutes: 30,
+ appointment_type: "presencial",
+ chief_complaint: formData.chief_complaint || "Consulta agendada pelo paciente",
+ patient_notes: formData.patient_notes || "",
+ created_by: patientId,
+ };
+
+ const response = await fetch(
+ `${supabaseUrl}/rest/v1/appointments`,
+ {
+ method: "POST",
+ headers: {
+ ...headers,
+ Prefer: "return=representation",
+ },
+ body: JSON.stringify(payload),
+ }
+ );
+
+ if (response.ok) {
+ const consultaCriada = await response.json();
+ console.log("Consulta criada:", consultaCriada);
+
+ setActiveStep(2);
+ setAgendando(false);
+
+ // Aqui você pode adicionar envio de SMS se necessário
+ // if (enviarSMS) {
+ // await sendSMS(telefone, mensagem, patientId);
+ // }
+
+ } else {
+ const error = await response.json();
+ console.error("Erro da API:", error);
+ throw new Error("Não foi possível criar a consulta");
+ }
+
+ } catch (error) {
+ console.error('Erro no agendamento:', error);
+ alert(error.message || "Erro ao realizar agendamento. Tente novamente.");
+ setAgendando(false);
+ }
+ };
+
+ const finalizarAgendamento = () => {
+ setModalConfirmacao(false);
+ navigate(`/${role}/consultalist`);
+ };
+
+ // Não precisamos mais da linha datasDisponiveis, pois usamos a Edge Function
+ const horariosDaData = horariosDisponiveis.filter(h => h.data === dataSelecionada);
+
+ const renderStepContent = (step) => {
+ switch (step) {
+ case 0:
+ return (
+
+
+ Confirme os dados da consulta:
+
+
+
+
+
+ Médico: Dr. {medico.nome}
+
+
+
+
+ Especialidade: {medico.especialidade}
+
+
+
+
+ Data: {new Date(dataSelecionada).toLocaleDateString('pt-BR')}
+
+
+
+
+ Horário: {horarioSelecionado ? horarioSelecionado.datetime.split("T")[1].substring(0, 5) : ''}
+
+
+
+ Valor: R$ {medico.valorConsulta}
+
+
+
+
+ Chegue com 15 minutos de antecedência para o atendimento.
+
+
+ );
+
+ case 1:
+ return (
+
+
+ Escolha como deseja receber as confirmações:
+
+
+ setEnviarEmail(e.target.checked)}
+ icon={}
+ checkedIcon={}
+ />
+ }
+ label="Receber confirmação por E-mail"
+ sx={{ mb: 2, display: 'block' }}
+ />
+
+ setEnviarSMS(e.target.checked)}
+ icon={}
+ checkedIcon={}
+ />
+ }
+ label="Receber confirmação por SMS"
+ sx={{ display: 'block' }}
+ />
+
+
+ Você também receberá um lembrete 24 horas antes da consulta.
+
+
+ );
+
+ case 2:
+ return (
+
+
+
+ Consulta Agendada com Sucesso!
+
+
+ Sua consulta foi agendada para {new Date(dataSelecionada).toLocaleDateString('pt-BR')} às {horarioSelecionado ? horarioSelecionado.datetime.split("T")[1].substring(0, 5) : ''}
+
+
+ A consulta foi adicionada à agenda do Dr. {medico.nome} e as confirmações foram enviadas.
+
+
+ );
+
+ default:
+ return null;
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+ Carregando horários...
+
+ );
+ }
+
+ if (!medico) {
+ return (
+
+ Médico não encontrado
+
+ );
+ }
+
+ return (
+
+
+ }
+ onClick={() => navigate("/paciente/medicosdisponiveis")}
+ className='btn btn-secondary'
+ >
+ Voltar para Médicos
+
+
+ {/* Cabeçalho do Médico */}
+
+
+
+
+ {medico.nome.split(' ').map(n => n[0]).join('')}
+
+
+
+ Dr. {medico.nome}
+
+
+
+ {medico.biografia}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default AgendarConsulta;
\ No newline at end of file
diff --git a/src/pages/PatientApp/MedicosDisponiveis.jsx b/src/pages/PatientApp/MedicosDisponiveis.jsx
new file mode 100644
index 0000000..e11a4b7
--- /dev/null
+++ b/src/pages/PatientApp/MedicosDisponiveis.jsx
@@ -0,0 +1,306 @@
+import React, { useState, useEffect } from 'react';
+import { getAccessToken } from '../../utils/auth';
+import {
+ Container,
+ Grid,
+ Card,
+ CardContent,
+ Typography,
+ Button,
+ Chip,
+ Avatar,
+ Box,
+ Rating,
+ CircularProgress,
+ TextField,
+ FormControl,
+ InputLabel,
+ Select,
+ MenuItem,
+ Paper,
+ InputAdornment,
+ Divider,
+ Alert
+} from '@mui/material';
+import {
+ ArrowBack,
+ CalendarToday,
+ AccessTime,
+ MedicalServices,
+ Search,
+ FilterList
+} from '@mui/icons-material';
+import { useNavigate } from 'react-router-dom';
+const AvatarForm = "/img/AvatarForm.jpg";
+import { getUserRole } from '../../utils/userInfo';
+
+const MedicosDisponiveis = () => {
+ const [medicos, setMedicos] = useState([]);
+ const [medicosFiltrados, setMedicosFiltrados] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchTerm, setSearchTerm] = useState('');
+ const [specialtyFilter, setSpecialtyFilter] = useState('');
+ const [avaliacaoFilter, setAvaliacaoFilter] = useState('');
+ const [valorFilter, setValorFilter] = useState('');
+ const navigate = useNavigate();
+ const role = getUserRole();
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const tokenUsuario = getAccessToken()
+ var myHeaders = new Headers();
+ myHeaders.append(
+ "apikey",
+ supabaseAK
+ );
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+
+ var requestOptions = {
+ method: "GET",
+ headers: myHeaders,
+ redirect: "follow",
+ };
+
+ // buscar médicos
+ useEffect(() => {
+ fetch(`${supabaseUrl}/rest/v1/doctors`, requestOptions)
+ .then((response) => response.json())
+ .then((result) => setMedicos(Array.isArray(result) ? result : []))
+ .catch((error) => console.log("error", error));
+ }, []);
+
+ useEffect(() => {
+ carregarMedicos();
+ }, []);
+
+ useEffect(() => {
+ aplicarFiltros();
+ }, [medicos, searchTerm, specialtyFilter, avaliacaoFilter, valorFilter]);
+
+ const carregarMedicos = async () => {
+ setLoading(true);
+ try {
+ setTimeout(() => {
+ setLoading(false);
+ }, 1000);
+
+ } catch (error) {
+ console.error('Erro ao carregar médicos:', error);
+ // Fallback para dados mock em caso de erro
+ setLoading(false);
+ }
+ };
+
+ const specialty = Array.from(new Set(medicos.map(m => m.specialty).filter(Boolean)));
+
+
+ const aplicarFiltros = () => {
+ let filtrados = [...medicos];
+
+ if (searchTerm) {
+ filtrados = filtrados.filter(medico => {
+ const nome = medico.full_name ? medico.full_name.toLowerCase() : "";
+ const especialidade = medico.specialty ? medico.specialty.toLowerCase() : "";
+ return (
+ nome.includes(searchTerm.toLowerCase()) ||
+ especialidade.includes(searchTerm.toLowerCase())
+ );
+ });
+ }
+
+ if (specialtyFilter) {
+ filtrados = filtrados.filter(medico =>
+ medico.specialty === specialtyFilter
+ );
+ }
+
+ setMedicosFiltrados(filtrados);
+ };
+
+ const limparFiltros = () => {
+ setSearchTerm('');
+ setSpecialtyFilter('');
+ setAvaliacaoFilter('');
+ setValorFilter('');
+ };
+
+ const verHorariosDisponiveis = (medicoId) => {
+ navigate(`/${role}/agendarconsulta/${medicoId}`);
+ };
+
+ if (loading) {
+ return (
+
+
+ Carregando médicos...
+
+ );
+ }
+
+ return (
+
+
+
+ Médicos Disponíveis
+
+
+ Encontre o médico perfeito para sua necessidade - {medicosFiltrados.length} médico(s) encontrado(s)
+
+
+ {/* Filtros e Busca */}
+
+
+
+ Filtros e Busca
+
+
+
+
+ setSearchTerm(e.target.value)}
+ InputProps={{
+ startAdornment: (
+
+
+
+ ),
+ }}
+ />
+
+
+
+
+ Especialidade
+ setSpecialtyFilter(e.target.value)}
+ >
+
+ {specialty.map(esp => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+ {/* Lista de Médicos */}
+
+ {medicosFiltrados.map((medico) => (
+
+
+
+ {/* Parte superior: avatar + texto */}
+
+
+
n[0]).join('')}
+ src={AvatarForm}
+ style={{ width: '80px', height: '80px', objectFit: 'cover' }}>
+
+
+
+
+ {medico.full_name}
+
+
+ }
+ label={medico.specialty}
+ color="primary"
+ variant="outlined"
+ size="small"
+ sx={{ mt: 1 }}
+ />
+
+
+ {/* Botão na parte inferior */}
+
+
+
+
+ ))}
+
+
+ {medicosFiltrados.length === 0 && !loading && (
+
+
+ Nenhum médico encontrado com os filtros selecionados
+
+
+
+ )}
+
+
+ );
+};
+
+export default MedicosDisponiveis;
\ No newline at end of file
diff --git a/src/pages/PatientApp/PatientApp.jsx b/src/pages/PatientApp/PatientApp.jsx
new file mode 100644
index 0000000..d363392
--- /dev/null
+++ b/src/pages/PatientApp/PatientApp.jsx
@@ -0,0 +1,80 @@
+import { Outlet, NavLink, useLocation } from "react-router-dom";
+import './../../assets/css/index.css'
+import { useState } from "react";
+import { getAccessToken} from "../../utils/auth";
+import { getUserRole } from '../../utils/userInfo.js';
+import Sidebar from "../../components/layouts/Sidebar.jsx";
+
+export default function PatientApp() {
+ const [isSidebarOpen, setSidebarOpen] = useState(false);
+ const location = useLocation();
+
+ // 2. Adicione a função para alternar o estado
+ const toggleSidebar = () => {
+ setSidebarOpen(!isSidebarOpen);
+ };
+
+ // 3. Crie a string de classe que será aplicada dinamicamente
+ const mainWrapperClass = isSidebarOpen ? 'main-wrapper sidebar-open' : 'main-wrapper';
+
+ // Função para verificar se a rota está ativa
+ const isActive = (path) => {
+ const currentPath = location.pathname;
+
+ // Verificação exata primeiro
+ if (currentPath === path) return true;
+
+ // Verificação de subrotas (ex: /patientapp/meuslaudos/view/123)
+ if (currentPath.startsWith(path + '/')) return true;
+
+ // Verificações específicas para páginas de edição/criação
+ if (path === '/patientapp/medicosdisponiveis' && (
+ currentPath.includes('/patientapp/agendar/') ||
+ currentPath.includes('/patientapp/consultaform')
+ )) return true;
+
+ if (path === '/patientapp/minhasconsultas' && (
+ currentPath.includes('/patientapp/consulta/') ||
+ currentPath.includes('/patientapp/editconsulta/')
+ )) return true;
+
+ if (path === '/patientapp/meuslaudos' && (
+ currentPath.includes('/patientapp/laudo/') ||
+ currentPath.includes('/patientapp/viewlaudo/')
+ )) return true;
+
+ return false;
+ };
+ const token = getAccessToken();
+ const user = getUserRole();
+ // Verificação de autenticação
+ if (!token) {
+ return
;
+ }
+
+ // Verificação de role
+ if (user !== 'paciente') {
+ return (
+
+
+
+
❌ Acesso Negado
+
Apenas administradores podem acessar esta área.
+
+
+
+
+ );
+ }
+ return (
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/pages/PatientApp/PatientDashboard.jsx b/src/pages/PatientApp/PatientDashboard.jsx
new file mode 100644
index 0000000..6ab6530
--- /dev/null
+++ b/src/pages/PatientApp/PatientDashboard.jsx
@@ -0,0 +1,887 @@
+import React, { useState, useEffect } from "react";
+import { Link } from "react-router-dom";
+import { getAccessToken } from "../../utils/auth.js";
+import { getFullName, getUserId } from "../../utils/userInfo";
+import "../../assets/css/index.css";
+import { getUserRole } from "../../utils/userInfo";
+
+const AvatarForm = "/img/AvatarForm.jpg";
+const banner = "/img/banner.png";
+
+export default function PatientDashboard() {
+ const [appointments, setAppointments] = useState([]);
+ const [reports, setReports] = useState([]);
+ const [nextConsultations, setNextConsultations] = useState([]);
+ const [recentExams, setRecentExams] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [currentTime, setCurrentTime] = useState(new Date());
+ const role = getUserRole();
+ const tokenUsuario = getAccessToken();
+ const userId = getUserId();
+ const patientName = getFullName() || "Paciente";
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const API_KEY = supabaseAK;
+
+ const requestOptions = {
+ method: "GET",
+ headers: {
+ apikey: API_KEY,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ redirect: "follow",
+ };
+
+ useEffect(() => {
+ const loadPatientData = async () => {
+ try {
+ setLoading(true);
+ console.log("🔄 Carregando dados do paciente...", { userId, tokenUsuario: !!tokenUsuario });
+
+ // Buscar todas as consultas primeiro (sem filtrar por patient_id se não existir na tabela)
+ const appointmentsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/appointments`,
+ requestOptions
+ );
+
+ const reportsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/reports`,
+ requestOptions
+ );
+
+ const doctorsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/doctors?select=id,full_name`,
+ requestOptions
+ );
+
+ console.log("📡 Status das respostas:", {
+ appointments: appointmentsResponse.status,
+ reports: reportsResponse.status,
+ doctors: doctorsResponse.status
+ });
+
+ const [appointmentsData, reportsData, doctorsData] = await Promise.all([
+ appointmentsResponse.json(),
+ reportsResponse.json(),
+ doctorsResponse.json()
+ ]);
+
+ console.log("📊 Dados recebidos:", {
+ appointments: appointmentsData,
+ reports: reportsData,
+ doctors: doctorsData
+ });
+
+ const appointmentsArr = Array.isArray(appointmentsData) ? appointmentsData : [];
+ const reportsArr = Array.isArray(reportsData) ? reportsData : [];
+ const doctorsArr = Array.isArray(doctorsData) ? doctorsData : [];
+
+ // Filtrar consultas por patient_id (se o campo existir)
+ const patientAppointments = appointmentsArr.filter(apt =>
+ apt.patient_id === userId ||
+ apt.patient_id === parseInt(userId) ||
+ // Se não tiver patient_id, mostrar algumas para demonstração
+ !apt.patient_id
+ );
+
+ // Filtrar relatórios por patient_id (se o campo existir)
+ const patientReports = reportsArr.filter(rep =>
+ rep.patient_id === userId ||
+ rep.patient_id === parseInt(userId) ||
+ // Se não tiver patient_id, mostrar alguns para demonstração
+ !rep.patient_id
+ );
+
+ // Enriquecer consultas com nomes dos médicos
+ const enrichedAppointments = patientAppointments.map(appointment => {
+ const doctor = doctorsArr.find(doc => doc.id === appointment.doctor_id);
+ return {
+ ...appointment,
+ doctor_name: doctor ? doctor.full_name : 'Médico não informado'
+ };
+ });
+
+ console.log("✅ Dados processados:", {
+ enrichedAppointments,
+ patientReports,
+ totalDoctors: doctorsArr.length
+ });
+
+ setAppointments(enrichedAppointments);
+ setReports(patientReports);
+
+ // Processar dados
+ console.log("🔥 TESTE: Chamando processNextConsultations com:", enrichedAppointments.length, "consultas");
+
+ // FORÇAR para teste
+ if (enrichedAppointments.length === 0) {
+ forceShowConsultations();
+ } else {
+ // Filtrar consultas não canceladas
+ const nonCancelledConsultations = enrichedAppointments.filter(apt =>
+ apt.status !== 'cancelled' &&
+ apt.status !== 'cancelada' &&
+ apt.status !== 'canceled'
+ );
+
+ console.log("📋 Consultas não canceladas:", nonCancelledConsultations.length, "de", enrichedAppointments.length);
+
+ if (nonCancelledConsultations.length > 0) {
+ // Ordenar por proximidade da data atual (mais próximas primeiro)
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+
+ const sortedByProximity = nonCancelledConsultations
+ .map(apt => {
+ const dateField = apt.scheduled_at || apt.date;
+ const timeField = apt.time;
+
+ if (dateField) {
+ let consultationDateTime;
+
+ if (dateField.includes('T')) {
+ // Data já inclui horário
+ consultationDateTime = new Date(dateField);
+ } else {
+ // Combinar data com horário
+ consultationDateTime = new Date(dateField);
+ if (timeField) {
+ const [hours, minutes] = timeField.split(':');
+ consultationDateTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
+ } else {
+ consultationDateTime.setHours(12, 0, 0, 0); // Default meio-dia se não houver horário
+ }
+ }
+
+ const now = new Date();
+ const diffInMinutes = Math.abs((consultationDateTime - now) / (1000 * 60));
+ return { ...apt, proximityScore: diffInMinutes, consultationDateTime };
+ }
+ return { ...apt, proximityScore: 999999 }; // Consultas sem data vão para o final
+ })
+ .sort((a, b) => a.proximityScore - b.proximityScore)
+ .slice(0, 2);
+
+ console.log("✅ Mostrando 2 consultas mais próximas da data atual:", sortedByProximity);
+ setNextConsultations(sortedByProximity);
+ } else {
+ console.log("⚠️ Todas as consultas estão canceladas - usando dados de teste");
+ forceShowConsultations();
+ }
+ }
+
+ processRecentExams(patientReports);
+
+ } catch (error) {
+ console.error("❌ Erro ao carregar dados do paciente:", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (tokenUsuario) {
+ loadPatientData();
+ }
+ }, [userId, tokenUsuario]);
+
+ // Processar próximas consultas
+ const processNextConsultations = (appointments) => {
+ console.log("🔄 Processando consultas:", appointments);
+ console.log("📊 Total de consultas recebidas:", appointments.length);
+
+ // Análise detalhada de cada consulta
+ appointments.forEach((apt, index) => {
+ console.log(`📋 Consulta ${index + 1}:`, {
+ id: apt.id,
+ scheduled_at: apt.scheduled_at,
+ date: apt.date,
+ time: apt.time,
+ doctor_name: apt.doctor_name,
+ status: apt.status
+ });
+ });
+
+ // Data de hoje em formato string para comparação
+ const today = new Date();
+ const todayString = today.toISOString().split('T')[0]; // YYYY-MM-DD
+
+ console.log("� Data de hoje (string):", todayString);
+
+ // Filtrar consultas futuras (incluindo hoje)
+ const futureConsultations = appointments.filter(apt => {
+ // Usar scheduled_at como data principal
+ const dateField = apt.scheduled_at || apt.date;
+
+ if (!dateField) {
+ console.log("⚠️ Consulta sem data:", apt.id);
+ return false;
+ }
+
+ // Normalizar a data da consulta
+ let consultationDate = dateField;
+
+ // Se a data contém horário, pegar apenas a parte da data
+ if (consultationDate.includes('T')) {
+ consultationDate = consultationDate.split('T')[0];
+ }
+
+ const isFuture = consultationDate >= todayString;
+ console.log(`📅 Consulta ${apt.id}: ${consultationDate} >= ${todayString} = ${isFuture}`);
+
+ return isFuture;
+ });
+
+ console.log("🔮 Consultas futuras encontradas:", futureConsultations.length);
+ console.log("📋 Lista de consultas futuras:", futureConsultations);
+
+ // Mostrar as 2 consultas mais próximas do horário atual (futuras ou passadas)
+ const consultationsWithProximity = appointments
+ .map(apt => {
+ const dateField = apt.scheduled_at || apt.date;
+ const timeField = apt.time;
+
+ if (dateField) {
+ let consultationDateTime;
+
+ if (dateField.includes('T')) {
+ // Data já inclui horário
+ consultationDateTime = new Date(dateField);
+ } else {
+ // Combinar data com horário
+ consultationDateTime = new Date(dateField);
+ if (timeField) {
+ const [hours, minutes] = timeField.split(':');
+ consultationDateTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
+ } else {
+ consultationDateTime.setHours(12, 0, 0, 0); // Default meio-dia se não houver horário
+ }
+ }
+
+ const now = new Date();
+ const diffInMinutes = Math.abs((consultationDateTime - now) / (1000 * 60));
+ return { ...apt, proximityScore: diffInMinutes, consultationDateTime };
+ }
+ return { ...apt, proximityScore: 999999 }; // Consultas sem data vão para o final
+ })
+ .sort((a, b) => a.proximityScore - b.proximityScore)
+ .slice(0, 2);
+
+ console.log("✅ 2 consultas mais próximas da data atual:", consultationsWithProximity);
+ setNextConsultations(consultationsWithProximity);
+ };
+
+ // FUNÇÃO DE TESTE - FORÇAR EXIBIÇÃO
+
+ // Processar exames recentes
+ const processRecentExams = (reports) => {
+ console.log("🔬 Processando exames:", reports);
+
+ // Ordenar por data de criação (mais recentes primeiro)
+ const recent = reports
+ .filter(report => report.created_at) // Apenas com data válida
+ .sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
+ .slice(0, 5);
+
+ console.log("✅ Exames recentes processados:", recent);
+ setRecentExams(recent);
+ };
+
+ // Atualizar relógio
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCurrentTime(new Date());
+ }, 1000);
+ return () => clearInterval(timer);
+ }, []);
+
+ // Funções auxiliares para status das consultas
+ const getStatusColor = (status) => {
+ switch (status) {
+ case 'confirmed': case 'confirmada': return 'bg-success';
+ case 'pending': case 'pendente': return 'bg-warning';
+ case 'cancelled': case 'cancelada': return 'bg-danger';
+ case 'completed': case 'finalizada': return 'bg-info';
+ default: return 'bg-primary';
+ }
+ };
+
+ const getStatusBorderColor = (status) => {
+ switch (status) {
+ case 'confirmed': case 'confirmada': return '#28a745';
+ case 'pending': case 'pendente': return '#ffc107';
+ case 'cancelled': case 'cancelada': return '#dc3545';
+ case 'completed': case 'finalizada': return '#17a2b8';
+ default: return '#007bff';
+ }
+ };
+
+ const getStatusIcon = (status) => {
+ switch (status) {
+ case 'confirmed': case 'confirmada': return 'fa-check';
+ case 'pending': case 'pendente': return 'fa-clock-o';
+ case 'cancelled': case 'cancelada': return 'fa-times';
+ case 'completed': case 'finalizada': return 'fa-check-circle';
+ default: return 'fa-calendar';
+ }
+ };
+
+
+
+
+
+ // Funções auxiliares para status dos exames
+ const getExamBorderColor = (status) => {
+ switch (status) {
+ case 'completed': case 'finalizado': return '#28a745';
+ case 'draft': case 'rascunho': return '#ffc107';
+ case 'pending': case 'pendente': return '#17a2b8';
+ default: return '#6c757d';
+ }
+ };
+
+ const getExamIconColor = (status) => {
+ switch (status) {
+ case 'completed': case 'finalizado': return 'bg-success';
+ case 'draft': case 'rascunho': return 'bg-warning';
+ case 'pending': case 'pendente': return 'bg-info';
+ default: return 'bg-secondary';
+ }
+ };
+
+ const getExamIcon = (status) => {
+ switch (status) {
+ case 'completed': case 'finalizado': return 'fa-check';
+ case 'draft': case 'rascunho': return 'fa-clock-o';
+ case 'pending': case 'pendente': return 'fa-file-text';
+ default: return 'fa-file-o';
+ }
+ };
+
+ const getExamBadgeClass = (status) => {
+ switch (status) {
+ case 'completed': case 'finalizado': return 'bg-success';
+ case 'draft': case 'rascunho': return 'bg-warning';
+ case 'pending': case 'pendente': return 'bg-info';
+ default: return 'bg-secondary';
+ }
+ };
+
+ const getExamStatusText = (status) => {
+ switch (status) {
+ case 'completed': case 'finalizado': return 'Concluído';
+ case 'draft': case 'rascunho': return 'Em análise';
+ case 'pending': case 'pendente': return 'Disponível';
+ default: return 'Processando';
+ }
+ };
+
+ // Estatísticas calculadas baseadas nos dados reais da API + demonstração (apenas consultas não canceladas)
+ const nonCancelledAppointments = appointments.filter(apt =>
+ apt.status !== 'cancelled' &&
+ apt.status !== 'cancelada' &&
+ apt.status !== 'canceled'
+ );
+
+ const totalConsultas = nonCancelledAppointments.length > 0 ? nonCancelledAppointments.length : 5;
+ const consultasRealizadas = nonCancelledAppointments.length > 0
+ ? nonCancelledAppointments.filter(apt => apt.status === 'completed' || apt.status === 'finalizada').length
+ : 3;
+ const proximasConsultas = nextConsultations.length;
+ const examesRealizados = reports.length > 0 ? reports.length : 3;
+ const [previewUrl, setPreviewUrl] = useState(AvatarForm);
+ useEffect(() => {
+ const loadAvatar = async () => {
+ if (!userId) return;
+
+ const myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+
+ const requestOptions = {
+ headers: myHeaders,
+ method: 'GET',
+ redirect: 'follow'
+ };
+
+ try {
+ const response = await fetch(`${supabaseUrl}/storage/v1/object/avatars/${userId}/avatar.png`, requestOptions);
+
+ if (response.ok) {
+ const blob = await response.blob();
+ const imageUrl = URL.createObjectURL(blob);
+ setPreviewUrl(imageUrl);
+ return; // Avatar encontrado
+ }
+ } catch (error) {
+
+ }
+
+ // Se chegou até aqui, não encontrou avatar - mantém o padrão
+
+ };
+
+ loadAvatar();
+ }, [userId]);
+
+
+ if (loading) {
+ return (
+
+
+
+
+
+ Carregando...
+
+
Carregando dashboard...
+
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Header com informações do paciente */}
+
+
+
+
+
+
+
👋 Olá, {patientName}!
+
Acompanhe suas consultas, resultados e tudo o que precisa em um só lugar.
+Cuide-se, e deixe o resto com a gente 💙
+
+ 🕒 {currentTime.toLocaleString('pt-BR')}
+
+
+
+

+
+
+
+
+
+
+
+ {/* Cards de estatísticas */}
+
+
+
+
+
+
+
+
{totalConsultas}
+ Total Consultas
+
+
+
+
+
+
+
+
+
+
+
{proximasConsultas}
+ Próximas Consultas
+
+
+
+
+
+
+
+
+
+
+
{examesRealizados}
+ Laudos
+
+
+
+
+
+
+
+
+
+
+
{consultasRealizadas}
+ Consultas Realizadas
+
+
+
+
+
+ {/* Ações rápidas */}
+
+
+
+
+
+
+
+
+
+ Agendar Consulta
+
+
+
+
+
+ Minhas Consultas
+
+
+
+
+
+ Meus Laudos
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Próximas Consultas */}
+
+
+
+
📅 Próximas Consultas
+
+ Ver todas
+
+
+
+ {nextConsultations.length > 0 ? (
+
+ {nextConsultations.map((consultation, index) => (
+
+
+
+
+
{consultation.doctor_name || 'Médico não informado'}
+
+ {(() => {
+ const dateToShow = consultation.scheduled_at || consultation.date;
+ if (dateToShow) {
+ // Extrair data e hora sem conversão de fuso horário
+ let dateStr = '';
+ let timeStr = '';
+
+ if (dateToShow.includes('T')) {
+ // Formato ISO: 2025-11-15T14:30:00
+ const [datePart, timePart] = dateToShow.split('T');
+ const [year, month, day] = datePart.split('-');
+ dateStr = `${day}/${month}/${year}`;
+
+ if (timePart) {
+ const [hour, minute] = timePart.split(':');
+ timeStr = `${hour}:${minute}`;
+ }
+ } else {
+ // Formato simples: 2025-11-15
+ const [year, month, day] = dateToShow.split('-');
+ dateStr = `${day}/${month}/${year}`;
+ }
+
+ // Usar horário do campo time se existir, senão usar o extraído
+ const finalTime = consultation.time || timeStr || 'Horário a confirmar';
+
+ return `${dateStr} às ${finalTime}`;
+ }
+ return 'Data a confirmar';
+ })()}
+
+
+
+ {consultation.status === 'requested' ? (
+ <>
+
+ Solicitado
+ >
+ ) : consultation.status === 'confirmed' ? (
+ <>
+
+ Confirmado
+ >
+ ) : consultation.status === 'completed' ? (
+ <>
+
+ Concluído
+ >
+ ) : consultation.status === 'cancelled' ? (
+ <>
+
+ Cancelado
+ >
+ ) : (
+ <>
+
+ {consultation.status}
+ >
+ )}
+
+
+
+ ))}
+
+ ) : (
+
+
+
Nenhuma consulta agendada
+
+ Agendar primeira consulta
+
+
+ )}
+
+ {/* Botão para ver mais */}
+
+
+
+ Agendar nova consulta
+
+
+
+
+
+
+ {/* Exames Recentes */}
+
+
+
+
🔬 Laudos Recentes
+
+ Ver todos
+
+
+
+ {recentExams.length > 0 ? (
+
+ {recentExams.map((exam) => (
+
+
+
+
+
{exam.exam || 'Exame'}
+
+ {new Date(exam.created_at).toLocaleDateString('pt-BR')} - {exam.requested_by || 'Médico não informado'}
+
+
+
+ {exam.status === 'draft' ? (
+ <>
+
+ Rascunho
+ >
+ ) : exam.status === 'completed' ? (
+ <>
+
+ Concluído
+ >
+ ) : (
+ exam.status
+ )}
+
+
+
+ ))}
+
+ ) : (
+
+
+
Nenhum laudo realizado ainda
+
+ )}
+
+
+
+
+ Ver todos os laudos
+
+
+
+
+
+
+
+ {/* Informações de saúde */}
+
+
+
+
+
💡 Dicas de Saúde
+
+
+
+
+
+
+
Exercite-se Regularmente
+
30 minutos de atividade física por dia fazem a diferença
+
+
+
+
+
+
Alimentação Saudável
+
Consuma frutas e vegetais todos os dias
+
+
+
+
+
+
Durma Bem
+
7-8 horas de sono são essenciais para sua saúde
+
+
+
+
+
+
+
+
+
+ );
+}
+
+// CSS customizado para o PatientDashboard
+const style = document.createElement('style');
+style.textContent = `
+ .timeline {
+ position: relative;
+ }
+
+ .timeline-item {
+ position: relative;
+ }
+
+ .timeline-marker {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ margin-top: 6px;
+ flex-shrink: 0;
+ }
+
+ .timeline-content {
+ background: #f8f9fa;
+ padding: 15px;
+ border-radius: 10px;
+ border-left: 3px solid #007bff;
+ width: 100%;
+ }
+
+ .exam-icon {
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ }
+
+ .health-tip {
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+ }
+
+ .health-tip:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15);
+ }
+
+ .dash-widget {
+ transition: transform 0.2s ease;
+ }
+
+ .dash-widget:hover {
+ transform: translateY(-3px);
+ }
+
+ .user-info-banner {
+ position: relative;
+ overflow: hidden;
+ }
+
+ .user-info-banner::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml,
');
+ pointer-events: none;
+ }
+`;
+
+if (!document.head.querySelector('[data-patient-dashboard-styles]')) {
+ style.setAttribute('data-patient-dashboard-styles', 'true');
+ document.head.appendChild(style);
+}
\ No newline at end of file
diff --git a/src/pages/Schedule/AddSchedule.jsx b/src/pages/Schedule/AddSchedule.jsx
deleted file mode 100644
index 900cdfa..0000000
--- a/src/pages/Schedule/AddSchedule.jsx
+++ /dev/null
@@ -1,242 +0,0 @@
-import "../../assets/css/index.css"
-
-function AddSchedule(){
- return (
-
-
-
- {/* Conteúdo da página */}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default AddSchedule;
diff --git a/src/pages/Schedule/DoctorSchedule.jsx b/src/pages/Schedule/DoctorSchedule.jsx
deleted file mode 100644
index 4334806..0000000
--- a/src/pages/Schedule/DoctorSchedule.jsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import "../../assets/css/index.css"
-import { Link } from "react-router-dom";
-
-function Doctorschedule() {
- return (
-
-
-
-
-
-
Agenda médica
-
-
-
- Adicionar agenda
-
-
-
-
-
-
-
-
-
-
- | Nome |
- Departamento |
- Dias disponíveis |
- Horário disponível |
- Status |
- Ação |
-
-
-
-
-
- {" "}
- Henry Daniels
- |
- Cardiologista |
- Segunda-feira, Terça-feira, Quinta-feira |
- 10:00 AM - 7:00 PM |
-
- Ativo
- |
-
-
- |
-
-
-
-
-
-
-
-
-
- {/* Modal de exclusão */}
-
-
-
-
-

-
Você tem certeza que deseja deletar essa agenda?
-
-
-
-
-
-
- );
-}
-export default Doctorschedule
diff --git a/src/pages/SecretariaApp/SecretariaApp.jsx b/src/pages/SecretariaApp/SecretariaApp.jsx
new file mode 100644
index 0000000..926eea0
--- /dev/null
+++ b/src/pages/SecretariaApp/SecretariaApp.jsx
@@ -0,0 +1,93 @@
+import { Outlet, NavLink, useLocation } from "react-router-dom";
+import './../../assets/css/index.css'
+import Navbar from '../../components/layouts/Navbar'
+import React, { useState } from 'react';
+import { Link } from "react-router-dom";
+import Chatbox from '../../components/chat/Chatbox';
+import AccessibilityWidget from '../../components/AccessibilityWidget';
+import { useResponsive } from '../../utils/useResponsive';
+import { getAccessToken } from '../../utils/auth.js';
+import { getUserRole } from '../../utils/userInfo.js';
+import { Navigate } from 'react-router-dom';
+import Sidebar from "../../components/layouts/Sidebar.jsx";
+
+
+function SecretariaApp() {
+ // 1. Adicione o estado para controlar a sidebar
+ const [isSidebarOpen, setSidebarOpen] = useState(false);
+ const location = useLocation();
+
+ // 2. Adicione a função para alternar o estado
+ const toggleSidebar = () => {
+ setSidebarOpen(!isSidebarOpen);
+ };
+
+ // 3. Crie a string de classe que será aplicada dinamicamente
+ const mainWrapperClass = isSidebarOpen ? 'main-wrapper sidebar-open' : 'main-wrapper';
+
+ // Função para verificar se a rota está ativa
+ const isActive = (path) => {
+ const currentPath = location.pathname;
+
+ // Verificação exata primeiro
+ if (currentPath === path) return true;
+
+ // Verificação de subrotas (ex: /secretaria/pacientelista/edit/123)
+ if (currentPath.startsWith(path + '/')) return true;
+
+ // Verificações específicas para páginas de edição/criação
+ if (path === '/secretaria/pacientelista' && (
+ currentPath.includes('/secretaria/pacienteeditar/') ||
+ currentPath.includes('/secretaria/pacienteform')
+ )) return true;
+
+ if (path === '/secretaria/medicoslista' && (
+ currentPath.includes('/secretaria/medicoseditar/')
+ )) return true;
+
+ if (path === '/secretaria/secretariaconsultalist' && (
+ currentPath.includes('/secretaria/editarconsulta/') ||
+ currentPath.includes('/secretaria/adicionarconsulta') ||
+ currentPath.includes('/secretaria/consulta/')
+ )) return true;
+ if (path === '/secretaria/agendamedica' && (
+ currentPath.includes('/secretaria/adicionaragenda')
+ )) return true;
+ return false;
+ };
+ const token = getAccessToken();
+ const user = getUserRole();
+ // Verificação de autenticação
+ if (!token) {
+ return
;
+ }
+
+ // Verificação de role
+ if (user !== 'secretaria') {
+ return (
+
+
+
+
❌ Acesso Negado
+
Apenas secretárias podem acessar esta área.
+
+
+
+
+ );
+ }
+ return (
+
+
+
+
+
+ );
+}
+
+export default SecretariaApp;
\ No newline at end of file
diff --git a/src/pages/SecretariaApp/SecretariaDashboard.jsx b/src/pages/SecretariaApp/SecretariaDashboard.jsx
new file mode 100644
index 0000000..f67da59
--- /dev/null
+++ b/src/pages/SecretariaApp/SecretariaDashboard.jsx
@@ -0,0 +1,845 @@
+import React, { useState, useEffect } from "react";
+import { Link } from "react-router-dom";
+import { getAccessToken } from "../../utils/auth.js";
+import "../../assets/css/index.css";
+import { getFullName, getUserId } from "../../utils/userInfo";
+const AvatarForm = "/img/AvatarForm.jpg";
+const banner = "/img/banner.png";
+import {
+ BarChart,
+ Bar,
+ PieChart,
+ Pie,
+ Cell,
+ XAxis,
+ YAxis,
+ CartesianGrid,
+ Tooltip,
+ Legend,
+ ResponsiveContainer
+} from 'recharts';
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip as ChartTooltip,
+ Legend as ChartLegend,
+} from 'chart.js';
+import { Bar as ChartJSBar } from 'react-chartjs-2';
+
+// Registrar componentes do Chart.js
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ ChartTooltip,
+ ChartLegend
+);
+
+// Componente do gráfico de consultas mensais
+const ConsultasMensaisChart = ({ data }) => (
+
+
+
+
+
+ [`${value} consultas`, 'Total']}
+ />
+
+
+
+
+);
+
+// Componente do gráfico de pacientes ativos/inativos
+const AtivosInativosChart = ({ data }) => (
+
+
+ `${name} ${(percent * 100).toFixed(0)}%`}
+ outerRadius={120}
+ fill="#8884d8"
+ dataKey="value"
+ >
+ {data.map((entry, index) => (
+ |
+ ))}
+
+ [`${value} pacientes`, name]}
+ />
+
+
+
+);
+
+// Componente do gráfico de consultas por médico com Chart.js (horizontal)
+const ConsultasPorMedicoChart = ({ data }) => {
+ if (!data || data.length === 0) {
+ return (
+
+
+
+
Nenhum dado de médicos encontrado
+
+
+ );
+ }
+
+ const chartData = {
+ labels: data.map(item => item.medico),
+ datasets: [
+ {
+ label: 'Consultas',
+ data: data.map(item => item.consultas),
+ backgroundColor: '#28a745',
+ borderColor: '#1e7e34',
+ borderWidth: 1,
+ borderRadius: 4,
+ borderSkipped: false,
+ }
+ ]
+ };
+
+ const options = {
+ indexAxis: 'y',
+ responsive: true,
+ maintainAspectRatio: false,
+ plugins: {
+ legend: {
+ display: false,
+ },
+ tooltip: {
+ backgroundColor: '#f8f9fa',
+ titleColor: '#343a40',
+ bodyColor: '#343a40',
+ borderColor: '#dee2e6',
+ borderWidth: 1,
+ callbacks: {
+ label: function (context) {
+ return `${context.parsed.x} consultas`;
+ }
+ }
+ }
+ },
+ scales: {
+ x: {
+ beginAtZero: true,
+ grid: {
+ color: '#e9ecef',
+ drawBorder: false,
+ },
+ ticks: {
+ color: '#6c757d',
+ font: {
+ size: 12
+ }
+ }
+ },
+ y: {
+ grid: {
+ display: false,
+ },
+ ticks: {
+ color: '#6c757d',
+ font: {
+ size: 11
+ },
+ maxRotation: 0,
+ }
+ }
+ },
+ animation: {
+ duration: 1000,
+ easing: 'easeInOutQuart'
+ },
+ layout: {
+ padding: {
+ left: 20,
+ right: 30,
+ top: 10,
+ bottom: 10
+ }
+ },
+ elements: {
+ bar: {
+ borderRadius: 4,
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+// Componente do gráfico de taxa de cancelamentos
+const TaxaCancelamentosChart = ({ data }) => {
+
+ if (!data || data.length === 0) {
+ return (
+
+
+
+
Nenhum dado de cancelamentos encontrado
+
+
+ );
+ }
+
+ // Preparar dados para Chart.js (gráfico empilhado)
+ const chartData = {
+ labels: data.map(item => item.mes),
+ datasets: [
+ {
+ label: 'Realizadas',
+ data: data.map(item => item.realizadas),
+ backgroundColor: '#dee2e6',
+ borderColor: '#adb5bd',
+ borderWidth: 1,
+ borderRadius: 4,
+ borderSkipped: false,
+ },
+ {
+ label: 'Canceladas',
+ data: data.map(item => item.canceladas),
+ backgroundColor: '#dc3545',
+ borderColor: '#c82333',
+ borderWidth: 1,
+ borderRadius: 4,
+ borderSkipped: false,
+ }
+ ]
+ };
+
+ const options = {
+ responsive: true,
+ maintainAspectRatio: false,
+ scales: {
+ x: {
+ stacked: true,
+ grid: {
+ display: false,
+ },
+ ticks: {
+ color: '#6c757d',
+ font: {
+ size: 12
+ }
+ }
+ },
+ y: {
+ stacked: true,
+ beginAtZero: true,
+ max: 100,
+ grid: {
+ color: '#e9ecef',
+ drawBorder: false,
+ },
+ ticks: {
+ color: '#6c757d',
+ font: {
+ size: 12
+ },
+ callback: function (value) {
+ return value + '%';
+ }
+ }
+ }
+ },
+ plugins: {
+ legend: {
+ display: true,
+ position: 'top',
+ labels: {
+ color: '#495057',
+ font: {
+ size: 12
+ },
+ usePointStyle: true,
+ pointStyle: 'rect'
+ }
+ },
+ tooltip: {
+ backgroundColor: '#f8f9fa',
+ titleColor: '#343a40',
+ bodyColor: '#343a40',
+ borderColor: '#dee2e6',
+ borderWidth: 1,
+ callbacks: {
+ label: function (context) {
+ const datasetLabel = context.dataset.label;
+ const value = context.parsed.y;
+ const dataIndex = context.dataIndex;
+ const monthData = data[dataIndex];
+
+ if (datasetLabel === 'Canceladas') {
+ const numConsultas = Math.round(monthData.total * value / 100);
+ return `${datasetLabel}: ${value}% (${numConsultas} de ${monthData.total} consultas)`;
+ } else {
+ const numConsultas = Math.round(monthData.total * value / 100);
+ return `${datasetLabel}: ${value}% (${numConsultas} consultas)`;
+ }
+ },
+ title: function (context) {
+ const monthData = data[context[0].dataIndex];
+ return `${context[0].label} ${new Date().getFullYear()} - Total: ${monthData.total} consultas`;
+ },
+ afterBody: function (context) {
+ const monthData = data[context[0].dataIndex];
+ if (monthData.total === 0) {
+ return ['Nenhuma consulta registrada neste mês'];
+ }
+ return [];
+ }
+ }
+ }
+ },
+ animation: {
+ duration: 1000,
+ easing: 'easeInOutQuart'
+ },
+ layout: {
+ padding: {
+ left: 10,
+ right: 10,
+ top: 10,
+ bottom: 10
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+function SecretariaDashboard() {
+ const [patients, setPatients] = useState([]);
+ const [doctors, setDoctors] = useState([]);
+ const [consulta, setConsulta] = useState([]);
+ const [countPaciente, setCountPaciente] = useState(0);
+ const [countMedico, setCountMedico] = useState(0);
+ // Estados para os gráficos
+ const [consultasMensaisDataReal, setConsultasMensaisDataReal] = useState([]);
+ const [pacientesStatusDataReal, setPacientesStatusDataReal] = useState([]);
+ const [consultasPorMedicoData, setConsultasPorMedicoData] = useState([]);
+ const [taxaCancelamentosData, setTaxaCancelamentosData] = useState([]);
+ const [appointments, setAppointments] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [currentTime, setCurrentTime] = useState(new Date());
+ const [previewUrl, setPreviewUrl] = useState(AvatarForm);
+
+ const tokenUsuario = getAccessToken();
+ const userId = getUserId();
+
+ const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
+ const supabaseAK = import.meta.env.VITE_SUPABASE_ANON_KEY;
+
+ const requestOptions = {
+ method: "GET",
+ headers: {
+ apikey:
+ supabaseAK,
+ Authorization: `Bearer ${tokenUsuario}`,
+ },
+ redirect: "follow",
+ };
+
+ useEffect(() => {
+ const loadData = async () => {
+ try {
+ setLoading(true);
+
+ // Buscar pacientes
+ const patientsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/patients`,
+ requestOptions
+ );
+ const patientsData = await patientsResponse.json();
+ const patientsArr = Array.isArray(patientsData) ? patientsData : [];
+ setPatients(patientsArr);
+ setConsulta(patientsArr);
+ setCountPaciente(patientsArr.length);
+
+ // Processar status dos pacientes
+ if (patientsArr.length > 0) {
+ const ativos = patientsArr.filter(p => p.active !== false).length;
+ const inativos = patientsArr.length - ativos;
+
+ const statusData = [
+ { name: 'Ativos', value: ativos, color: '#007bff' },
+ { name: 'Inativos', value: inativos, color: '#ffa500' }
+ ];
+
+ setPacientesStatusDataReal(statusData);
+ }
+
+ // Buscar médicos
+ const doctorsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/doctors`,
+ requestOptions
+ );
+ const doctorsData = await doctorsResponse.json();
+ const doctorsArr = Array.isArray(doctorsData) ? doctorsData : [];
+ setDoctors(doctorsArr);
+ setCountMedico(doctorsArr.length);
+
+ // Buscar consultas
+ const appointmentsResponse = await fetch(
+ `${supabaseUrl}/rest/v1/appointments`,
+ requestOptions
+ );
+ const appointmentsData = await appointmentsResponse.json();
+ const appointmentsArr = Array.isArray(appointmentsData) ? appointmentsData : [];
+ setAppointments(appointmentsArr);
+
+ // Processar dados dos gráficos
+ processConsultasMensais(appointmentsArr);
+ await processConsultasPorMedico(appointmentsArr, doctorsArr);
+ processTaxaCancelamentos(appointmentsArr);
+
+
+ } catch (error) {
+
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ loadData();
+ }, []);
+
+ // useEffect para atualizar o relógio em tempo real
+ useEffect(() => {
+ const timer = setInterval(() => {
+ setCurrentTime(new Date());
+ }, 1000);
+
+ return () => clearInterval(timer);
+ }, []);
+
+ // useEffect para carregar avatar do usuário (mesma lógica da navbar)
+ useEffect(() => {
+ const loadAvatar = async () => {
+ if (!userId) return;
+
+ const myHeaders = new Headers();
+ myHeaders.append("apikey", supabaseAK);
+ myHeaders.append("Authorization", `Bearer ${tokenUsuario}`);
+
+ const requestOptions = {
+ headers: myHeaders,
+ method: 'GET',
+ redirect: 'follow'
+ };
+
+ try {
+ const response = await fetch(`${supabaseUrl}/storage/v1/object/avatars/${userId}/avatar.png`, requestOptions);
+
+ if (response.ok) {
+ const blob = await response.blob();
+ const imageUrl = URL.createObjectURL(blob);
+ setPreviewUrl(imageUrl);
+ return;
+ }
+ } catch (error) {
+
+ }
+
+ };
+
+ loadAvatar();
+ }, [userId]);
+
+ // Processar dados das consultas mensais
+ const processConsultasMensais = (appointmentsData) => {
+ const meses = [
+ 'Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun',
+ 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'
+ ];
+
+ const consultasPorMes = meses.map(mes => ({ mes, consultas: 0 }));
+
+ if (appointmentsData && appointmentsData.length > 0) {
+ appointmentsData.forEach(appointment => {
+ if (appointment.scheduled_at) {
+ const data = new Date(appointment.scheduled_at);
+ const mesIndex = data.getMonth();
+ if (mesIndex >= 0 && mesIndex < 12) {
+ consultasPorMes[mesIndex].consultas++;
+ }
+ }
+ });
+ }
+
+ setConsultasMensaisDataReal(consultasPorMes);
+ };
+
+ // Processar dados das consultas por médico
+ const processConsultasPorMedico = async (appointmentsData, doctorsData) => {
+ try {
+ // Criar mapa de médicos
+ const doctorsMap = {};
+ doctorsData.forEach(doctor => {
+ let doctorName = doctor.full_name || doctor.name || `Médico ${doctor.id}`;
+ doctorName = doctorName.trim();
+ doctorsMap[doctor.id] = doctorName;
+ });
+
+ // Contar consultas por médico
+ const consultasPorMedico = {};
+ appointmentsData.forEach(appointment => {
+ if (appointment.doctor_id) {
+ const doctorName = doctorsMap[appointment.doctor_id] || `Médico ${appointment.doctor_id}`;
+ consultasPorMedico[doctorName] = (consultasPorMedico[doctorName] || 0) + 1;
+ }
+ });
+
+ // Converter para array e ordenar por número de consultas (maior para menor)
+ const chartData = Object.entries(consultasPorMedico)
+ .map(([medico, consultas]) => ({ medico, consultas }))
+ .sort((a, b) => b.consultas - a.consultas)
+ .slice(0, 10); // Mostrar apenas os top 10 médicos
+
+ setConsultasPorMedicoData(chartData);
+ } catch (error) {
+ setConsultasPorMedicoData([]);
+ }
+ };
+
+ // Processar dados da taxa de cancelamentos
+ const processTaxaCancelamentos = (appointmentsData) => {
+ const meses = [
+ 'Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun',
+ 'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez'
+ ];
+
+ const cancelamentosPorMes = meses.map(mes => ({
+ mes,
+ realizadas: 0,
+ canceladas: 0,
+ total: 0
+ }));
+
+ if (appointmentsData && appointmentsData.length > 0) {
+
+ appointmentsData.forEach(appointment => {
+ if (appointment.scheduled_at) {
+ const data = new Date(appointment.scheduled_at);
+ const mesIndex = data.getMonth();
+ const anoAtual = new Date().getFullYear();
+ const anoConsulta = data.getFullYear();
+
+ // Processar apenas consultas do ano atual
+ if (mesIndex >= 0 && mesIndex < 12 && anoConsulta === anoAtual) {
+ cancelamentosPorMes[mesIndex].total++;
+
+ // Verificar diferentes possíveis campos de status de cancelamento
+ const isCancelled =
+ appointment.status === 'cancelled' ||
+ appointment.status === 'canceled' ||
+ appointment.cancelled === true ||
+ appointment.is_cancelled === true ||
+ appointment.appointment_status === 'cancelled' ||
+ appointment.appointment_status === 'canceled';
+
+ if (isCancelled) {
+ cancelamentosPorMes[mesIndex].canceladas++;
+ } else {
+ cancelamentosPorMes[mesIndex].realizadas++;
+ }
+ }
+ }
+ });
+
+ // Calcular porcentagens e manter valores absolutos para tooltip
+ cancelamentosPorMes.forEach(mes => {
+ if (mes.total > 0) {
+ const realizadasCount = mes.realizadas;
+ const canceladasCount = mes.canceladas;
+
+ mes.realizadas = Math.round((realizadasCount / mes.total) * 100);
+ mes.canceladas = Math.round((canceladasCount / mes.total) * 100);
+
+ // Garantir que soma seja 100%
+ if (mes.realizadas + mes.canceladas !== 100 && mes.total > 0) {
+ mes.realizadas = 100 - mes.canceladas;
+ }
+ } else {
+ // Se não há dados, mostrar 100% realizadas
+ mes.realizadas = 100;
+ mes.canceladas = 0;
+ }
+ });
+
+
+ setTaxaCancelamentosData(cancelamentosPorMes);
+ } else {
+ // Se não há dados da API, deixar vazio
+
+ setTaxaCancelamentosData([]);
+ }
+ };
+
+
+
+ return (
+
+
+ {/* Header com informações da secretária */}
+
+
+
+
+
+
+
📋 Olá, {getFullName()}!
+
O MediConnect está pronto para mais um dia de organização e cuidado. Continue ajudando nossa clínica a funcionar de forma leve, eficiente e acolhedora!
+
+ 🕒 {currentTime.toLocaleString('pt-BR')}
+
+
+
+

+
+
+
+
+
+
+
+ {/* Cards de estatísticas */}
+
+
+
+
+
+
+
+
{countPaciente}
+ Total Pacientes
+
+
+
+
+
+
+
+
+
+
+
{countMedico}
+ Total Médicos
+
+
+
+
+
+
+
+
+
+
+
{appointments.length}
+ Total Consultas
+
+
+
+
+
+
+
+
+
+
+
80
+ Atendidos
+
+
+
+
+ {/* Seção dos Gráficos */}
+
+ {/* Consultas por Mês */}
+
+
+
+
📊 Consultas por Mês ({new Date().getFullYear()})
+
+
+ {loading ? (
+
+
+
+
Carregando dados...
+
+
+ ) : consultasMensaisDataReal.length > 0 ? (
+
+ ) : (
+
+
+
+
Nenhum dado encontrado
+
+
+ )}
+
+
+
+
+ {/* Top 10 Médicos */}
+
+
+
+
🏆 Top 10 Médicos (Consultas)
+
+
+ {loading ? (
+
+
+
+
Carregando dados...
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ {/* Pacientes Ativos/Inativos */}
+
+
+
+
👥 Pacientes Ativos x Inativos
+
+
+ {loading ? (
+
+
+
+
Carregando dados...
+
+
+ ) : pacientesStatusDataReal.length > 0 ? (
+
+ ) : (
+
+
+
+
Nenhum dado encontrado
+
+
+ )}
+
+
+
+
+ {/* Taxa de Cancelamentos */}
+
+
+
+
📉 Taxa de Cancelamentos
+
+
+ {loading ? (
+
+
+
+
Carregando dados...
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ );
+}
+
+// CSS customizado para o SecretariaDashboard (mesmo estilo do AdminDashboard)
+const style = document.createElement('style');
+style.textContent = `
+ .user-info-banner {
+ position: relative;
+ overflow: hidden;
+ }
+
+ .user-info-banner::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml,
');
+ pointer-events: none;
+ }
+
+ .dash-widget {
+ transition: transform 0.2s ease;
+ }
+
+ .dash-widget:hover {
+ transform: translateY(-3px);
+ }
+
+ .card {
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+ }
+
+ .card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15) !important;
+ }
+`;
+
+if (!document.head.querySelector('[data-secretaria-dashboard-styles]')) {
+ style.setAttribute('data-secretaria-dashboard-styles', 'true');
+ document.head.appendChild(style);
+}
+
+export default SecretariaDashboard;
\ No newline at end of file
diff --git a/src/pages/laudos/Laudo.jsx b/src/pages/laudos/Laudo.jsx
deleted file mode 100644
index 9c82b9d..0000000
--- a/src/pages/laudos/Laudo.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-// PatientList.jsx
-import { useEditor, EditorContent } from '@tiptap/react'
-import StarterKit from '@tiptap/starter-kit'
-import Bar from '../../components/Bar';
-import Image from '@tiptap/extension-image';
-
-function Laudo() {
- const editor = useEditor({
- extensions: [StarterKit, Image],
- content: ""
- })
- const comandos = {
- toggleBold: () => editor.chain().focus().toggleBold().run(),
- toggleItalic: () => editor.chain().focus().toggleItalic().run(),
- toggleUnderline: () => editor.chain().focus().toggleUnderline().run(),
- toggleCodeBlock: () => editor.chain().focus().toggleCodeBlock().run(),
- toggleH1: () => editor.chain().focus().toggleHeading({ level: 1 }).run(),
- toggleH2: () => editor.chain().focus().toggleHeading({ level: 2 }).run(),
- toggleH3: () => editor.chain().focus().toggleHeading({ level: 3 }).run(),
- toggleParrafo: () => editor.chain().focus().setParagraph().run(),
- toggleListaOrdenada: () => editor.chain().focus().toggleOrderedList().run(),
- toggleListaPuntos: () => editor.chain().focus().toggleBulletList().run(),
- agregarImagen: () => {
- const url = window.prompt('URL da imagem')
- editor.chain().focus().setImage({ src: url }).run();
-
- },
- agregarLink: () => {
- const url = window.prompt('URL do link')
- if (url) {
- editor.chain().focus().setLink({ href: url }).run()
- }
- }
- }
-
- return (
-
- );
-}
-
-export default Laudo;
-;
\ No newline at end of file
diff --git a/src/pages/laudos/LaudosList.jsx b/src/pages/laudos/LaudosList.jsx
deleted file mode 100644
index bee3797..0000000
--- a/src/pages/laudos/LaudosList.jsx
+++ /dev/null
@@ -1,288 +0,0 @@
-import { Link } from "react-router-dom";
-import "../../assets/css/index.css";
-import React, { useState, useRef, useLayoutEffect, useEffect } from "react";
-import { createPortal } from "react-dom";
-
-function DropdownPortal({ anchorEl, isOpen, onClose, className, children }) {
- const menuRef = useRef(null);
- const [stylePos, setStylePos] = useState({
- position: "absolute",
- top: 0,
- left: 0,
- visibility: "hidden",
- zIndex: 1000,
- });
-
- useLayoutEffect(() => {
- if (!isOpen || !anchorEl || !menuRef.current) return;
-
- const anchorRect = anchorEl.getBoundingClientRect();
- const menuRect = menuRef.current.getBoundingClientRect();
- const scrollY = window.scrollY || window.pageYOffset;
- const scrollX = window.scrollX || window.pageXOffset;
-
- let left = anchorRect.right + scrollX - menuRect.width;
- let top = anchorRect.bottom + scrollY;
-
- if (left < 0) left = scrollX + 4;
- if (top + menuRect.height > window.innerHeight + scrollY) {
- top = anchorRect.top + scrollY - menuRect.height;
- }
-
- setStylePos({
- position: "absolute",
- top: `${Math.round(top)}px`,
- left: `${Math.round(left)}px`,
- visibility: "visible",
- zIndex: 1000,
- });
- }, [isOpen, anchorEl, children]);
-
- useEffect(() => {
- if (!isOpen) return;
-
- const handleDocClick = (e) => {
- if (menuRef.current && !menuRef.current.contains(e.target) &&
- anchorEl && !anchorEl.contains(e.target)) {
- onClose();
- }
- };
- const handleScroll = () => onClose();
-
- document.addEventListener("mousedown", handleDocClick);
- document.addEventListener("scroll", handleScroll, true);
-
- return () => {
- document.removeEventListener("mousedown", handleDocClick);
- document.removeEventListener("scroll", handleScroll, true);
- };
- }, [isOpen, onClose, anchorEl]);
-
- if (!isOpen) return null;
- return createPortal(
-
e.stopPropagation()}>
- {children}
-
,
- document.body
- );
-}
-
-function LaudoList() {
- const [search, setSearch] = useState("");
- const [period, setPeriod] = useState(""); // "", "today", "week", "month"
- const [startDate, setStartDate] = useState("");
- const [endDate, setEndDate] = useState("");
- const [laudos, setLaudos] = useState([
- {
- id: 1,
- pedido: 12345,
- data: "2024-10-01",
- prazo: "2024-10-05",
- paciente: "Davi Andrade",
- cpf: "12345678900",
- tipo: "Radiologia",
- status: "Pendente",
- executante: "Dr. Silva",
- exame: "Raio-X de Tórax"
- },
- {
- id: 2,
- pedido: 12346,
- data: "2024-10-02",
- prazo: "2024-10-06",
- paciente: "Maria Souza",
- cpf: "98765432100",
- tipo: "Cardiologia",
- status: "Concluído",
- executante: "Dra. Lima",
- exame: "Eletrocardiograma"
- },
- {
- id: 3,
- pedido: 12347,
- data: "2024-10-03",
- prazo: "2024-10-07",
- paciente: "João Pereira",
- cpf: "45678912300",
- tipo: "Neurologia",
- status: "Em Andamento",
- executante: "Dr. Costa",
- exame: "Ressonância Magnética"
- },
- {
- id: 4,
- pedido: 12348,
- data: "2024-10-04",
- prazo: "2024-10-08",
- paciente: "Ana Oliveira",
- cpf: "32165498700",
- tipo: "Ortopedia",
- status: "Pendente",
- executante: "Dra. Fernandes",
- exame: "Tomografia Computadorizada"
- },
- ]);
- const [openDropdown, setOpenDropdown] = useState(null);
- const anchorRefs = useRef({});
-
- const handleDelete = (id) => {
- if (!window.confirm("Tem certeza que deseja excluir este laudo?")) return;
- setLaudos(prev => prev.filter(l => l.id !== id));
- setOpenDropdown(null);
- };
-
- const filteredLaudos = laudos.filter(l => {
- const q = search.toLowerCase();
- const textMatch =
- (l.paciente || "").toLowerCase().includes(q) ||
- (l.cpf || "").toLowerCase().includes(q) ||
- (l.tipo || "").toLowerCase().includes(q) ||
- (l.status || "").toLowerCase().includes(q) ||
- (l.pedido || "").toString().toLowerCase().includes(q) ||
- (l.prazo || "").toLowerCase().includes(q) ||
- (l.executante || "").toLowerCase().includes(q) ||
- (l.exame || "").toLowerCase().includes(q) ||
- (l.data || "").toLowerCase().includes(q);
-
- let dateMatch = true;
- const today = new Date();
- const laudoDate = new Date(l.data);
-
- if (period === "today") {
- dateMatch = laudoDate.toDateString() === today.toDateString();
- } else if (period === "week") {
- const startOfWeek = new Date(today);
- startOfWeek.setDate(today.getDate() - today.getDay());
- const endOfWeek = new Date(startOfWeek);
- endOfWeek.setDate(startOfWeek.getDate() + 6);
- dateMatch = laudoDate >= startOfWeek && laudoDate <= endOfWeek;
- } else if (period === "month") {
- dateMatch = laudoDate.getMonth() === today.getMonth() && laudoDate.getFullYear() === today.getFullYear();
- }
-
- if (startDate && endDate) {
- dateMatch = dateMatch && l.data >= startDate && l.data <= endDate;
- } else if (startDate) {
- dateMatch = dateMatch && l.data >= startDate;
- } else if (endDate) {
- dateMatch = dateMatch && l.data <= endDate;
- }
-
- return textMatch && dateMatch;
- });
-
- const mascararCPF = (cpf = "") => {
- if (cpf.length < 5) return cpf;
- return `${cpf.slice(0,3)}.***.***-${cpf.slice(-2)}`;
- };
-
- return (
-
-
-
-
Laudos
-
- {/* Linha de pesquisa e filtros */}
-
- {/* Esquerda: pesquisa */}
-
- setSearch(e.target.value)}
- style={{ minWidth: "200px" }}
- />
-
-
- {/* Direita: filtros de data + botões */}
-
-
-
- {/* Tabela */}
-
-
-
-
-
-
- | Pedido |
- Data |
- Prazo |
- Paciente |
- CPF |
- Tipo |
- Status |
- Executante |
- Exame |
- Ações |
-
-
-
- {filteredLaudos.length>0 ? filteredLaudos.map(l=>(
-
- | {l.pedido} |
- {l.data} |
- {l.prazo} |
- {l.paciente} |
- {mascararCPF(l.cpf)} |
- {l.tipo} |
- {l.status} |
- {l.executante} |
- {l.exame} |
-
-
-
- setOpenDropdown(null)} className="dropdown-menu dropdown-menu-right show">
- {e.stopPropagation(); setOpenDropdown(null);}}>
- Laudo
-
-
-
-
- |
-
- )) : (
-
- | Nenhum laudo encontrado |
-
- )}
-
-
-
-
-
-
-
-
-
- );
-}
-
-export default LaudoList;
-
diff --git a/src/routes/AdminRoutes.jsx b/src/routes/AdminRoutes.jsx
new file mode 100644
index 0000000..c281e97
--- /dev/null
+++ b/src/routes/AdminRoutes.jsx
@@ -0,0 +1,60 @@
+import { AdminApp } from "../pages/AdminApp/AdminApp";
+import AdminDashboard from "../pages/AdminApp/AdminDashboard"
+import CreateUser from "../pages/AdminApp/CreateUser";
+//listas
+import AgendaDoctor from "../components/lists/AgendaDoctor"
+import ConsultaList from "../components/lists/ConsultaList";
+import DoctorList from "../components/lists/DoctorList";
+import PatientList from "../components/lists/PatientList";
+import LaudoList from "../components/lists/LaudoList";
+//forms
+import AgendaForm from "../components/forms/AgendaForm";
+import ConsultaForm from "../components/forms/ConsultaForm";
+import PatientForm from "../components/forms/PatientForm";
+import DoctorForm from "../components/forms/DoctorForm";
+import LaudoForm from "../components/forms/LaudoForm";
+//edits
+import DoctorEdit from "../components/edits/DoctorEdit";
+import PatientEdit from "../components/edits/PatientEdit";
+import ConsultaEdit from "../components/edits/ConsultaEdit";
+import LaudoEdit from "../components/edits/LaudoEdit";
+import VerLaudo from "../components/VerLaudo";
+
+import Doctorexcecao from "../pages/DoctorApp/Doctorexceçao";
+import LaudoConsulta from "../components/LaudoConsulta";
+
+
+export const AdminRoutes =
+ {
+ path: "/admin",
+ element:
,
+ children: [
+ {index: true, element:
},
+ {path: "dashboard", element:
},
+ //listas
+ {path: "agendadoctor", element:
},
+ {path: "consultalist", element:
},
+ {path: "doctorlist", element:
},
+ {path: "patientlist", element:
},
+ {path: "laudolist", element:
},
+ //forms
+ {path: "agendaform", element:
},
+ {path: "consultaform", element:
},
+ {path: "patientform", element:
},
+ {path: "doctorform", element:
},
+ {path: "laudoform", element:
},
+ {path: "laudoconsulta", element:
},
+
+
+
+ //edits
+ {path: "editdoctor/:id", element:
},
+ {path: "editpatient/:id", element:
},
+ {path: "editconsulta/:id", element:
},
+ {path: "editlaudo/:id", element:
},
+ //create user
+ {path: "createuser", element:
},
+ {path: "excecao", element:
},
+ {path: "verlaudo/:id", element:
},
+ ]
+};
diff --git a/src/routes/DoctorRoutes.jsx b/src/routes/DoctorRoutes.jsx
new file mode 100644
index 0000000..8ea3679
--- /dev/null
+++ b/src/routes/DoctorRoutes.jsx
@@ -0,0 +1,50 @@
+import DoctorDashBoard from "../pages/DoctorApp/DoctorDashboard";
+import DoctorApp from "../pages/DoctorApp/DoctorApp";
+import DoctorProntuario from "../pages/DoctorApp/Prontuario/DoctorProntuario";
+import DoctorProntuarioList from "../pages/DoctorApp/Prontuario/DoctorProntuarioList"
+import Doctorexeceçao from "../pages/DoctorApp/Doctorexceçao";
+
+import AgendaDoctor from "../components/lists/AgendaDoctor"
+import ConsultaList from "../components/lists/ConsultaList";
+import PatientList from "../components/lists/PatientList";
+import LaudoList from "../components/lists/LaudoList";
+//forms
+import AgendaForm from "../components/forms/AgendaForm";
+import ConsultaForm from "../components/forms/ConsultaForm";
+import LaudoForm from "../components/forms/LaudoForm";
+//edits
+import ConsultaEdit from "../components/edits/ConsultaEdit";
+import LaudoEdit from "../components/edits/LaudoEdit";
+import VerLaudo from "../components/VerLaudo";
+import DoctorCalendar from "../pages/DoctorApp/DoctorCalendar";
+import LaudoConsulta from "../components/LaudoConsulta";
+
+export const DoctorRoutes =
+ {path : "/medico",
+ element:
,
+ children: [
+ {index: true, element:
},
+ {path: "dashboard", element:
},
+ {path: "prontuario", element:
},
+ {path: "prontuariolist", element:
},
+ {path: "calendar", element:
},
+ {path: "excecao", element:
},
+ //listas
+ {path: "agendadoctor", element:
},
+ {path: "consultalist", element:
},
+ {path: "patientlist", element:
},
+ {path: "laudolist", element:
},
+ //forms
+ {path: "agendaform", element:
},
+ {path: "consultaform", element:
},
+ {path: "laudoform", element:
},
+ {path: "laudoconsulta", element:
},
+
+ //edits
+ {path: "editconsulta/:id", element:
},
+ {path: "editlaudo/:id", element:
},
+ {path: "verlaudo/:id", element:
},
+ {path: "doctorcalendar", element:
},
+
+ ]
+};
\ No newline at end of file
diff --git a/src/routes/PatientRoutes.jsx b/src/routes/PatientRoutes.jsx
new file mode 100644
index 0000000..b30bd79
--- /dev/null
+++ b/src/routes/PatientRoutes.jsx
@@ -0,0 +1,29 @@
+import PatientDashboard from "../pages/PatientApp/PatientDashboard";
+import PatientApp from "../pages/PatientApp/PatientApp";
+
+//listas
+import LaudoList from "../components/lists/LaudoList";
+import ConsultaList from "../components/lists/ConsultaList";
+
+//forms
+import MedicosDisponiveis from "../pages/PatientApp/MedicosDisponiveis";
+import AgendarConsulta from "../pages/PatientApp/AgendarConsultas";
+import VerLaudo from "../components/VerLaudo";
+
+
+export const PatientRoutes =
+ {
+ path: "/paciente",
+ element:
,
+ children: [
+ {index: true, element:
},
+ {path: "dashboard", element:
},
+ //listas
+ {path: "laudolist", element:
},
+ {path: "consultalist", element:
},
+ //forms
+ {path: "medicosdisponiveis", element:
},
+ {path: "agendarconsulta/:medicoId", element:
},
+ {path: "verlaudo/:id", element:
},
+ ]
+ };
diff --git a/src/routes/RoutesApp.jsx b/src/routes/RoutesApp.jsx
new file mode 100644
index 0000000..fe4dc79
--- /dev/null
+++ b/src/routes/RoutesApp.jsx
@@ -0,0 +1,45 @@
+import { createBrowserRouter } from "react-router-dom";
+import { AdminRoutes } from "./AdminRoutes";
+import { DoctorRoutes } from "./DoctorRoutes";
+import { PatientRoutes } from "./PatientRoutes";
+import { SecretariaRoutes } from "./SecretariaRoutes";
+import Login from "../pages/Login/Login.jsx";
+import MagicLink from "../pages/Login/Acessounico.jsx";
+import HospitalLanding from "../pages/LandingPage/HospitalLanding.jsx"
+import RoomPage from "../components/call/RoomPage.jsx";
+import Chat from "../components/chat.jsx";
+export const router = createBrowserRouter([
+ {
+ path: "/",
+ element:
+ },
+
+ {
+ path: "/login",
+ element:
+ },
+ {
+ path: "/AcessoUnico",
+ element:
+ },
+ {
+ path: "/call/:roomId",
+ element:
+ },
+ {
+ path: "/chat",
+ element:
+ },
+
+ // ✅ Se AdminRoutes for função:
+ AdminRoutes,
+ DoctorRoutes,
+ PatientRoutes,
+ SecretariaRoutes,
+
+ // ✅ OU se AdminRoutes for objeto:
+ // AdminRoutes,
+ // DoctorRoutes,
+ // PatientRoutes,
+ // SecretariaRoutes,
+]);
\ No newline at end of file
diff --git a/src/routes/SecretariaRoutes.jsx b/src/routes/SecretariaRoutes.jsx
new file mode 100644
index 0000000..64e930b
--- /dev/null
+++ b/src/routes/SecretariaRoutes.jsx
@@ -0,0 +1,37 @@
+import SecretariaApp from "../pages/SecretariaApp/SecretariaApp";
+import SecretariaDashboard from "../pages/SecretariaApp/SecretariaDashboard";
+
+import AgendaDoctor from "../components/lists/AgendaDoctor"
+import ConsultaList from "../components/lists/ConsultaList";
+import DoctorList from "../components/lists/DoctorList";
+import PatientList from "../components/lists/PatientList";
+//forms
+import AgendaForm from "../components/forms/AgendaForm";
+import ConsultaForm from "../components/forms/ConsultaForm";
+import PatientForm from "../components/forms/PatientForm";
+
+//edits
+import PatientEdit from "../components/edits/PatientEdit";
+import ConsultaEdit from "../components/edits/ConsultaEdit";
+
+export const SecretariaRoutes =
+ {
+ path: "/secretaria",
+ element:
,
+ children: [
+ {index: true, element:
},
+ {path: "dashboard", element:
},
+ //listas
+ {path: "agendadoctor", element:
},
+ {path: "consultalist", element:
},
+ {path: "doctorlist", element:
},
+ {path: "patientlist", element:
},
+ //forms
+ {path: "agendaform", element:
},
+ {path: "consultaform", element:
},
+ {path: "patientform", element:
},
+ //edits
+ {path: "editpatient/:id", element:
},
+ {path: "editconsulta/:id", element:
},
+ ]
+ };
diff --git a/src/utils/InterimMark.js b/src/utils/InterimMark.js
new file mode 100644
index 0000000..29798a0
--- /dev/null
+++ b/src/utils/InterimMark.js
@@ -0,0 +1,34 @@
+import { Mark, mergeAttributes } from '@tiptap/core';
+
+export const InterimMark = Mark.create({
+ name: 'interimMark',
+
+ // Isso faz com que a marca seja "exclusiva".
+ // Se você aplicar 'bold' e 'interimMark', o Tiptap
+ // saberá como lidar com isso.
+ inclusive: false,
+
+ // Isso define como a marca será renderizada no HTML
+ renderHTML({ HTMLAttributes }) {
+ return [
+ 'span',
+ mergeAttributes(HTMLAttributes, {
+ 'data-interim': 'true',
+ // O estilo "provisório" que você queria.
+ // Cinza e itálico dão uma boa ideia de "pre-confirm".
+ 'style': 'color: #888; font-style: italic;',
+ }),
+ 0, // 0 significa "aqui vai o conteúdo (texto)"
+ ];
+ },
+
+ // Isso define como o Tiptap deve ler essa marca do HTML
+ // (útil se você for carregar conteúdo salvo)
+ parseHTML() {
+ return [
+ {
+ tag: 'span[data-interim]',
+ },
+ ];
+ },
+});
\ No newline at end of file
diff --git a/src/utils/auth.js b/src/utils/auth.js
new file mode 100644
index 0000000..0963db9
--- /dev/null
+++ b/src/utils/auth.js
@@ -0,0 +1,39 @@
+export function getAccessToken() {
+ return localStorage.getItem("access_token");
+}
+
+export function getRefreshToken() {
+ return localStorage.getItem("refresh_token");
+}
+
+export async function refreshAccessToken() {
+ const refresh_token = getRefreshToken();
+ if (!refresh_token) return null;
+
+ const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token", {
+ method: "POST",
+ headers: {
+ "apikey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ", // substitua pela sua chave real
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ grant_type: "refresh_token",
+ refresh_token
+ })
+ });
+
+ if (!response.ok) {
+ console.error("Erro ao atualizar token:", response.status, await response.text());
+ return null;
+ }
+
+ const data = await response.json();
+
+ if (data.access_token) {
+ localStorage.setItem("access_token", data.access_token);
+ if (data.refresh_token) localStorage.setItem("refresh_token", data.refresh_token);
+ return data.access_token;
+ }
+
+ return null;
+}
diff --git a/src/utils/permissions.js b/src/utils/permissions.js
new file mode 100644
index 0000000..1d1036f
--- /dev/null
+++ b/src/utils/permissions.js
@@ -0,0 +1,13 @@
+function permissions(role) {
+ const permissoes = {
+ admin: ['dashboard', 'consultas', 'usuarios', 'consultaform'],
+ medico: ['dashboard', 'consultas', ''],
+ recepcionista: ['dashboard']
+ };
+ return permissoes[role] || [];
+}
+
+function pode(role, acao) {
+ const permissoesRole = permissions(role);
+ return permissoesRole.includes(acao);
+}
\ No newline at end of file
diff --git a/src/utils/sendSMS.js b/src/utils/sendSMS.js
new file mode 100644
index 0000000..398e8a0
--- /dev/null
+++ b/src/utils/sendSMS.js
@@ -0,0 +1,33 @@
+import { getAccessToken } from "../utils/auth";
+
+export async function sendSMS(phoneNumber, message, patientId) {
+ const token = getAccessToken();
+
+ const headers = new Headers();
+ headers.append("Authorization", `Bearer ${token}`);
+ headers.append("apikey", "SUA_ANON_KEY_REAL_DO_SUPABASE"); // substitua pela sua anon key real
+ headers.append("Content-Type", "application/json");
+
+ // 🔹 garante formato internacional (+55)
+ const formattedNumber = phoneNumber.startsWith("+")
+ ? phoneNumber
+ : `+55${phoneNumber.replace(/\D/g, "")}`;
+
+ const body = JSON.stringify({
+ phone_number: formattedNumber,
+ message,
+ patient_id: patientId,
+ });
+
+ const response = await fetch(
+ "https://yuanqfswhberkoevtmfr.supabase.co/functions/v1/send-sms",
+ { method: "POST", headers, body }
+ );
+
+ if (!response.ok) {
+ const text = await response.text().catch(() => "");
+ throw new Error(`Falha ao enviar SMS (${response.status}) ${text}`);
+ }
+
+ return response.json();
+}
\ No newline at end of file
diff --git a/src/utils/sidebar.js b/src/utils/sidebar.js
new file mode 100644
index 0000000..e0c16f7
--- /dev/null
+++ b/src/utils/sidebar.js
@@ -0,0 +1,120 @@
+ var Sidemenu = function() {
+ this.$menuItem = $('#sidebar-menu a');
+ };
+
+ function init() {
+ var $this = Sidemenu;
+ $('#sidebar-menu a').on('click', function(e) {
+ if($(this).parent().hasClass('submenu')) {
+ e.preventDefault();
+ }
+ if(!$(this).hasClass('subdrop')) {
+ $('ul', $(this).parents('ul:first')).slideUp(350);
+ $('a', $(this).parents('ul:first')).removeClass('subdrop');
+ $(this).next('ul').slideDown(350);
+ $(this).addClass('subdrop');
+ } else if($(this).hasClass('subdrop')) {
+ $(this).removeClass('subdrop');
+ $(this).next('ul').slideUp(350);
+ }
+ });
+ $('#sidebar-menu ul li.submenu a.active').parents('li:last').children('a:first').addClass('active').trigger('click');
+ }
+ // Sidebar Initiate
+ init();
+
+ // Sidebar overlay
+ function sidebar_overlay($target) {
+ if($target.length) {
+ $target.toggleClass('opened');
+ $sidebarOverlay.toggleClass('opened');
+ $('html').toggleClass('menu-opened');
+ $sidebarOverlay.attr('data-reff', '#' + $target[0].id);
+ }
+ }
+
+ // Mobile menu sidebar overlay
+ $(document).on('click', '#mobile_btn', function() {
+ var $target = $($(this).attr('href'));
+ sidebar_overlay($target);
+ $wrapper.toggleClass('slide-nav');
+ $('#chat_sidebar').removeClass('opened');
+ return false;
+ });
+
+ // Chat sidebar overlay
+ $(document).on('click', '#task_chat', function() {
+ var $target = $($(this).attr('href'));
+ console.log($target);
+ sidebar_overlay($target);
+ return false;
+ });
+
+ // Sidebar overlay reset
+ $sidebarOverlay.on('click', function() {
+ var $target = $($(this).attr('data-reff'));
+ if($target.length) {
+ $target.removeClass('opened');
+ $('html').removeClass('menu-opened');
+ $(this).removeClass('opened');
+ $wrapper.removeClass('slide-nav');
+ }
+ return false;
+ });
+
+ // Select 2
+ if($('.select').length > 0) {
+ $('.select').select2({
+ minimumResultsForSearch: -1,
+ width: '100%'
+ });
+ }
+
+ // Floating Label
+ if($('.floating').length > 0) {
+ $('.floating').on('focus blur', function(e) {
+ $(this).parents('.form-focus').toggleClass('focused', (e.type === 'focus' || this.value.length > 0));
+ }).trigger('blur');
+ }
+
+ // Right Sidebar Scroll
+ if($('#msg_list').length > 0) {
+ $('#msg_list').slimscroll({
+ height: '100%',
+ color: '#878787',
+ disableFadeOut: true,
+ borderRadius: 0,
+ size: '4px',
+ alwaysVisible: false,
+ touchScrollStep: 100
+ });
+ var msgHeight = $(window).height() - 124;
+ $('#msg_list').height(msgHeight);
+ $('.msg-sidebar .slimScrollDiv').height(msgHeight);
+ $(window).resize(function() {
+ var msgrHeight = $(window).height() - 124;
+ $('#msg_list').height(msgrHeight);
+ $('.msg-sidebar .slimScrollDiv').height(msgrHeight);
+ });
+ }
+
+ // Left Sidebar Scroll
+ if($slimScrolls.length > 0) {
+ $slimScrolls.slimScroll({
+ height: 'auto',
+ width: '100%',
+ position: 'right',
+ size: '7px',
+ color: '#ccc',
+ wheelStep: 10,
+ touchScrollStep: 100
+ });
+ var wHeight = $(window).height() - 60;
+ $slimScrolls.height(wHeight);
+ $('.sidebar .slimScrollDiv').height(wHeight);
+ $(window).resize(function() {
+ var rHeight = $(window).height() - 60;
+ $slimScrolls.height(rHeight);
+ $('.sidebar .slimScrollDiv').height(rHeight);
+ });
+ }
\ No newline at end of file
diff --git a/src/utils/sweetalertTheme.js b/src/utils/sweetalertTheme.js
new file mode 100644
index 0000000..33bcae1
--- /dev/null
+++ b/src/utils/sweetalertTheme.js
@@ -0,0 +1,24 @@
+// src/utils/sweetalertTheme.js
+import Swal from "sweetalert2";
+
+export function themedSwal(options) {
+ const isDark = document.body.classList.contains("dark-mode");
+
+ const baseOptions = {
+ background: isDark ? "#1e293b" : "#ffffff",
+ color: isDark ? "#e2e8f0" : "#111827",
+ confirmButtonColor: "#3399ff",
+ cancelButtonColor: isDark ? "#475569" : "#6c757d",
+ customClass: {
+ popup: isDark ? "swal2-dark-popup" : "",
+ title: "swal2-title",
+ confirmButton: "swal2-confirm",
+ cancelButton: "swal2-cancel",
+ },
+ };
+
+ return Swal.fire({
+ ...baseOptions,
+ ...options,
+ });
+}
diff --git a/src/utils/useResponsive.js b/src/utils/useResponsive.js
new file mode 100644
index 0000000..55352ff
--- /dev/null
+++ b/src/utils/useResponsive.js
@@ -0,0 +1,199 @@
+import { useMediaQuery } from 'react-responsive';
+import { useMemo, useEffect, useState } from 'react';
+
+// Breakpoints mais abrangentes e modernos
+export const BREAKPOINTS = {
+ // Mobile First Approach
+ xs: '(max-width: 479px)', // Mobile pequeno
+ sm: '(min-width: 480px) and (max-width: 767px)', // Mobile grande
+ md: '(min-width: 768px) and (max-width: 1023px)', // Tablet
+ lg: '(min-width: 1024px) and (max-width: 1279px)', // Desktop pequeno
+ xl: '(min-width: 1280px) and (max-width: 1439px)', // Desktop médio
+ xxl: '(min-width: 1440px)', // Desktop grande
+ '2xl': '(min-width: 1920px)', // Desktop extra grande
+
+ // Breakpoints úteis para comportamentos específicos
+ touchDevice: '(hover: none) and (pointer: coarse)',
+ stylusDevice: '(hover: none) and (pointer: fine)',
+ mouseDevice: '(hover: hover) and (pointer: fine)',
+
+ // Para modo alto contraste/dark mode preferido
+ prefersDark: '(prefers-color-scheme: dark)',
+ prefersLight: '(prefers-color-scheme: light)',
+ prefersReducedMotion: '(prefers-reduced-motion: reduce)'
+};
+
+// Sistema de breakpoints para mobile-first
+export const BREAKPOINTS_MOBILE_FIRST = {
+ xs: 480, // > 480px
+ sm: 768, // > 768px
+ md: 1024, // > 1024px
+ lg: 1280, // > 1280px
+ xl: 1440, // > 1440px
+ xxl: 1920 // > 1920px
+};
+
+export const useResponsive = () => {
+ // Breakpoints principais
+ const isXS = useMediaQuery({ query: BREAKPOINTS.xs });
+ const isSM = useMediaQuery({ query: BREAKPOINTS.sm });
+ const isMD = useMediaQuery({ query: BREAKPOINTS.md });
+ const isLG = useMediaQuery({ query: BREAKPOINTS.lg });
+ const isXL = useMediaQuery({ query: BREAKPOINTS.xl });
+ const isXXL = useMediaQuery({ query: BREAKPOINTS.xxl });
+ const is2XL = useMediaQuery({ query: BREAKPOINTS['2xl'] });
+
+ // Dispositivos e capacidades
+ const isTouchDevice = useMediaQuery({ query: BREAKPOINTS.touchDevice });
+ const isStylusDevice = useMediaQuery({ query: BREAKPOINTS.stylusDevice });
+ const isMouseDevice = useMediaQuery({ query: BREAKPOINTS.mouseDevice });
+
+ // Preferências do usuário
+ const prefersDark = useMediaQuery({ query: BREAKPOINTS.prefersDark });
+ const prefersLight = useMediaQuery({ query: BREAKPOINTS.prefersLight });
+ const prefersReducedMotion = useMediaQuery({ query: BREAKPOINTS.prefersReducedMotion });
+
+ // Estado para evitar hidratação inconsistente no SSR
+ const [isClient, setIsClient] = useState(false);
+
+ useEffect(() => {
+ setIsClient(true);
+ }, []);
+
+ // Device type com lógica mais robusta
+ const deviceType = useMemo(() => {
+ if (!isClient) return 'ssr'; // Para SSR
+
+ if (isXS) return 'xs';
+ if (isSM) return 'sm';
+ if (isMD) return 'md';
+ if (isLG) return 'lg';
+ if (isXL) return 'xl';
+ if (isXXL) return 'xxl';
+ if (is2XL) return '2xl';
+
+ return 'unknown';
+ }, [isClient, isXS, isSM, isMD, isLG, isXL, isXXL, is2XL]);
+
+ // Agrupamentos úteis
+ const isMobile = useMemo(() => isXS || isSM, [isXS, isSM]);
+ const isTablet = useMemo(() => isMD, [isMD]);
+ const isDesktop = useMemo(() => isLG || isXL || isXXL || is2XL, [isLG, isXL, isXXL, is2XL]);
+ const isLargeScreen = useMemo(() => isXL || isXXL || is2XL, [isXL, isXXL, is2XL]);
+
+ // Helpers para orientação de layout
+ const layoutType = useMemo(() => {
+ if (isMobile) return 'mobile';
+ if (isTablet) return 'tablet';
+ if (isDesktop) return 'desktop';
+ return 'unknown';
+ }, [isMobile, isTablet, isDesktop]);
+
+ // Retorno memoizado para performance
+ return useMemo(() => ({
+ // Breakpoints individuais
+ isXS,
+ isSM,
+ isMD,
+ isLG,
+ isXL,
+ isXXL,
+ is2XL,
+
+ // Agrupamentos
+ isMobile,
+ isTablet,
+ isDesktop,
+ isLargeScreen,
+
+ // Tipo de dispositivo e layout
+ deviceType,
+ layoutType,
+
+ // Capacidades do dispositivo
+ isTouchDevice,
+ isStylusDevice,
+ isMouseDevice,
+
+ // Preferências do usuário
+ prefersDark,
+ prefersLight,
+ prefersReducedMotion,
+
+ // Helpers para condições comuns
+ isPortrait: typeof window !== 'undefined' ? window.innerHeight > window.innerWidth : false,
+ isLandscape: typeof window !== 'undefined' ? window.innerWidth > window.innerHeight : false,
+
+ // Métodos úteis
+ breakpoint: deviceType,
+ isAbove: (breakpoint) => {
+ const breakpoints = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl', '2xl'];
+ const currentIndex = breakpoints.indexOf(deviceType);
+ const targetIndex = breakpoints.indexOf(breakpoint);
+ return currentIndex > targetIndex;
+ },
+ isBelow: (breakpoint) => {
+ const breakpoints = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl', '2xl'];
+ const currentIndex = breakpoints.indexOf(deviceType);
+ const targetIndex = breakpoints.indexOf(breakpoint);
+ return currentIndex < targetIndex;
+ }
+ }), [
+ isXS, isSM, isMD, isLG, isXL, isXXL, is2XL,
+ isMobile, isTablet, isDesktop, isLargeScreen,
+ deviceType, layoutType,
+ isTouchDevice, isStylusDevice, isMouseDevice,
+ prefersDark, prefersLight, prefersReducedMotion
+ ]);
+};
+
+// Hook específico para orientação melhorado
+export const useOrientation = () => {
+ const [orientation, setOrientation] = useState({
+ isPortrait: false,
+ isLandscape: false,
+ angle: 0
+ });
+
+ useEffect(() => {
+ const updateOrientation = () => {
+ const isPortrait = window.innerHeight > window.innerWidth;
+ const screenOrientation = window.screen?.orientation || {};
+
+ setOrientation({
+ isPortrait,
+ isLandscape: !isPortrait,
+ angle: screenOrientation.angle || 0,
+ type: screenOrientation.type || (isPortrait ? 'portrait' : 'landscape')
+ });
+ };
+
+ updateOrientation();
+
+ window.addEventListener('resize', updateOrientation);
+ window.addEventListener('orientationchange', updateOrientation);
+
+ return () => {
+ window.removeEventListener('resize', updateOrientation);
+ window.removeEventListener('orientationchange', updateOrientation);
+ };
+ }, []);
+
+ return orientation;
+};
+
+// Hook para debug (apenas desenvolvimento)
+export const useResponsiveDebug = () => {
+ const responsive = useResponsive();
+
+ useEffect(() => {
+ if (process.env.NODE_ENV === 'development') {
+ console.log('📱 Responsive Debug:', responsive);
+ }
+ }, [responsive]);
+
+ return responsive;
+};
+
+// Export default para compatibilidade
+export default useResponsive;
\ No newline at end of file
diff --git a/src/utils/userInfo.js b/src/utils/userInfo.js
new file mode 100644
index 0000000..8ecf874
--- /dev/null
+++ b/src/utils/userInfo.js
@@ -0,0 +1,69 @@
+// userInfo.js
+export function setUserId(id) {
+ localStorage.setItem("user_id", id);
+}
+
+
+export function getUserId() {
+ return localStorage.getItem("user_id");
+}
+
+
+export function setUserEmail(email) {
+ localStorage.setItem("user_email", email);
+}
+
+
+export function getUserEmail() {
+ return localStorage.getItem("user_email");
+}
+
+
+export function setUserRole(role) {
+ localStorage.setItem("user_role", role);
+}
+
+
+export function getUserRole() {
+ return localStorage.getItem("user_role");
+}
+
+
+export function setDoctorId(doctorId) {
+ localStorage.setItem("doctor_id", doctorId);
+}
+
+
+export function getDoctorId() {
+ return localStorage.getItem("doctor_id");
+}
+
+
+export function setPatientId(patientId) {
+ localStorage.setItem("patient_id", patientId);
+}
+
+
+export function getPatientId() {
+ return localStorage.getItem("patient_id");
+}
+
+
+export function setFullName(fullName) {
+ localStorage.setItem("full_name", fullName);
+}
+
+
+export function getFullName() {
+ return localStorage.getItem("full_name");
+}
+
+
+export function clearUserInfo() {
+ localStorage.removeItem("user_id");
+ localStorage.removeItem("user_email");
+ localStorage.removeItem("user_role");
+ localStorage.removeItem("doctor_id");
+ localStorage.removeItem("patient_id");
+ localStorage.removeItem("full_name");
+}
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
index 8b0f57b..e46ae86 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,7 +1,11 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
- plugins: [react()],
+ plugins: [
+ react(),
+ tailwindcss(),
+ ]
})