Atualizar

This commit is contained in:
guisilvagomes 2025-10-24 12:03:40 -03:00
parent 2d8fcb5b4a
commit eae5e8cb92
73 changed files with 2802 additions and 5002 deletions

View File

@ -0,0 +1,76 @@
const axios = require("axios");
const ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
(async () => {
try {
console.log("🔐 Fazendo login como admin...");
const loginRes = await axios.post(
`${BASE_URL}/auth/v1/token?grant_type=password`,
{
email: "riseup@popcode.com.br",
password: "riseup",
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
console.log("✅ Login admin bem-sucedido!\n");
const token = loginRes.data.access_token;
// Buscar o ID do Fernando no profiles
console.log("🔍 Buscando ID do Fernando...");
const profileRes = await axios.get(
`${BASE_URL}/rest/v1/profiles?email=eq.fernando.pirichowski@souunit.com.br&select=*`,
{
headers: {
apikey: ANON_KEY,
Authorization: `Bearer ${token}`,
},
}
);
if (profileRes.data.length === 0) {
console.log("❌ Fernando não encontrado no profiles");
return;
}
const fernandoId = profileRes.data[0].id;
console.log("✅ Fernando encontrado! ID:", fernandoId);
// Criar entrada na tabela patients
console.log("\n📋 Criando entrada na tabela patients...");
const patientRes = await axios.post(
`${BASE_URL}/rest/v1/patients`,
{
id: fernandoId,
email: "fernando.pirichowski@souunit.com.br",
full_name: "Fernando Pirichowski",
phone_mobile: "51999999999",
cpf: "12345678909", // CPF válido fictício
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
Authorization: `Bearer ${token}`,
Prefer: "return=representation",
},
}
);
console.log("✅ Entrada na tabela patients criada!");
console.log("\n🎉 Usuário Fernando Pirichowski completo!");
console.log("📧 Email: fernando.pirichowski@souunit.com.br");
console.log("🔑 Senha: fernando123");
console.log("\n✨ Agora você pode testar a recuperação de senha!");
} catch (err) {
console.error("❌ Erro:", err.response?.data || err.message);
}
})();

View File

@ -0,0 +1,58 @@
const axios = require("axios");
const ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
(async () => {
try {
console.log("🔐 Fazendo login como admin...");
const loginRes = await axios.post(
`${BASE_URL}/auth/v1/token?grant_type=password`,
{
email: "riseup@popcode.com.br",
password: "riseup",
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
console.log("✅ Login admin bem-sucedido!");
const token = loginRes.data.access_token;
console.log("\n🔍 Buscando usuário fernando...");
const usersRes = await axios.get(`${BASE_URL}/rest/v1/profiles?select=*`, {
headers: {
apikey: ANON_KEY,
Authorization: `Bearer ${token}`,
},
});
console.log(`\n📊 Total de usuários: ${usersRes.data.length}`);
const fernando = usersRes.data.find(
(u) =>
u.email &&
(u.email.toLowerCase().includes("fernando") ||
u.full_name?.toLowerCase().includes("fernando"))
);
if (fernando) {
console.log("\n✅ Usuário Fernando encontrado:");
console.log(JSON.stringify(fernando, null, 2));
} else {
console.log("\n❌ Usuário Fernando NÃO encontrado na tabela profiles");
console.log("\n📧 Alguns emails cadastrados:");
usersRes.data.slice(0, 15).forEach((u) => {
if (u.email)
console.log(` - ${u.email} (${u.full_name || "sem nome"})`);
});
}
} catch (err) {
console.error("❌ Erro:", err.response?.data || err.message);
}
})();

View File

@ -0,0 +1,87 @@
const axios = require("axios");
const ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
(async () => {
try {
console.log("🔐 Fazendo login como admin...");
const loginRes = await axios.post(
`${BASE_URL}/auth/v1/token?grant_type=password`,
{
email: "riseup@popcode.com.br",
password: "riseup",
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
console.log("✅ Login admin bem-sucedido!\n");
const token = loginRes.data.access_token;
console.log("👤 Criando usuário Fernando Pirichowski...");
// Criar usuário via signup
const signupRes = await axios.post(
`${BASE_URL}/auth/v1/signup`,
{
email: "fernando.pirichowski@souunit.com.br",
password: "fernando123", // Senha temporária
options: {
data: {
full_name: "Fernando Pirichowski",
phone: "51999999999",
},
},
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
console.log("✅ Usuário criado com sucesso!");
console.log("📧 Email:", signupRes.data.user.email);
console.log("🆔 ID:", signupRes.data.user.id);
console.log("🔑 Senha temporária: fernando123\n");
// Criar entrada na tabela patients
console.log("📋 Criando entrada na tabela patients...");
const patientRes = await axios.post(
`${BASE_URL}/rest/v1/patients`,
{
id: signupRes.data.user.id,
email: "fernando.pirichowski@souunit.com.br",
full_name: "Fernando Pirichowski",
phone_mobile: "51999999999",
cpf: "12345678909", // CPF válido fictício
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
Authorization: `Bearer ${token}`,
Prefer: "return=representation",
},
}
);
console.log("✅ Entrada na tabela patients criada!");
console.log("\n🎉 Usuário Fernando Pirichowski criado com sucesso!");
console.log("📧 Email: fernando.pirichowski@souunit.com.br");
console.log("🔑 Senha: fernando123");
console.log("\n💡 Agora você pode testar a recuperação de senha!");
} catch (err) {
console.error("❌ Erro:", err.response?.data || err.message);
if (err.response?.data?.msg) {
console.error("Mensagem:", err.response.data.msg);
}
}
})();

View File

@ -1,11 +1,42 @@
<!doctype html>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="https://lumi.new/lumi.ing/logo.png" />
<link rel="icon" href="/logo.PNG" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MediConnect</title>
<title>MediConnect - Sistema de Agendamento Médico</title>
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content="https://mediconnectbrasil.app/" />
<meta
property="og:title"
content="MediConnect - Sistema de Agendamento Médico"
/>
<meta
property="og:description"
content="Conectando pacientes e profissionais de saúde com eficiência e segurança"
/>
<meta
property="og:image"
content="https://mediconnectbrasil.app/logo.PNG"
/>
<!-- Twitter -->
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://mediconnectbrasil.app/" />
<meta
property="twitter:title"
content="MediConnect - Sistema de Agendamento Médico"
/>
<meta
property="twitter:description"
content="Conectando pacientes e profissionais de saúde com eficiência e segurança"
/>
<meta
property="twitter:image"
content="https://mediconnectbrasil.app/logo.PNG"
/>
</head>
<body>
<div id="root"></div>

View File

@ -1,24 +0,0 @@
[build]
command = "pnpm build"
publish = "dist"
[functions]
directory = "netlify/functions"
[dev]
command = "npm run dev"
targetPort = 5173
port = 8888
autoLaunch = false
framework = "#custom"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
# Optional: control caching of static assets
[[headers]]
for = "/assets/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"

View File

@ -1,163 +0,0 @@
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return { statusCode: 200, headers, body: "" };
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token n<>o fornecido" }),
};
}
const pathParts = event.path.split("/");
const appointmentId =
pathParts[pathParts.length - 1] !== "appointments"
? pathParts[pathParts.length - 1]
: null;
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/appointments`;
if (appointmentId && appointmentId !== "appointments") {
url += `?id=eq.${appointmentId}&select=*`;
} else if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: { apikey: SUPABASE_ANON_KEY, Authorization: authHeader },
});
let data = await response.json();
if (
appointmentId &&
appointmentId !== "appointments" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
if (!body.patient_id || !body.doctor_id || !body.scheduled_at) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigat<61>rios: patient_id, doctor_id, scheduled_at",
}),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/appointments`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
let data = await response.json();
if (Array.isArray(data) && data.length > 0) data = data[0];
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "PATCH") {
if (!appointmentId || appointmentId === "appointments") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do agendamento <20> obrigat<61>rio" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/appointments?id=eq.${appointmentId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) data = data[0];
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "DELETE") {
if (!appointmentId || appointmentId === "appointments") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do agendamento <20> obrigat<61>rio" }),
};
}
const response = await fetch(
`${SUPABASE_URL}/rest/v1/appointments?id=eq.${appointmentId}`,
{
method: "DELETE",
headers: { apikey: SUPABASE_ANON_KEY, Authorization: authHeader },
}
);
return { statusCode: response.status, headers, body: "" };
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: "Erro interno" }),
};
}
};

View File

@ -1,153 +0,0 @@
/**
* Netlify Function: Listar Atribuições
* GET /rest/v1/patient_assignments
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
// GET - Listar atribuições
if (event.httpMethod === "GET") {
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Monta URL com query params (se houver)
const queryString = event.queryStringParameters
? "?" +
new URLSearchParams(
event.queryStringParameters as Record<string, string>
).toString()
: "";
const response = await fetch(
`${SUPABASE_URL}/rest/v1/patient_assignments${queryString}`,
{
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro ao listar atribuições:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
}
// POST - Criar atribuição
if (event.httpMethod === "POST") {
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
const body = JSON.parse(event.body || "{}");
if (!body.patient_id || !body.user_id || !body.role) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "patient_id, user_id e role são obrigatórios",
}),
};
}
const response = await fetch(
`${SUPABASE_URL}/rest/v1/patient_assignments`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro ao criar atribuição:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
};

View File

@ -46,6 +46,12 @@ export const handler: Handler = async (event: HandlerEvent) => {
const body: LoginRequest = JSON.parse(event.body || "{}");
if (!body.email || !body.password) {
// Log headers and raw body to help debugging malformed requests from frontend
console.error(
"[auth-login] Requisição inválida - falta email ou password. Headers:",
event.headers
);
console.error("[auth-login] Raw body:", event.body);
return {
statusCode: 400,
headers,

View File

@ -1,90 +0,0 @@
/**
* Netlify Function: Logout
* Invalida a sessão do usuário no Supabase
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
// Pega o Bearer token do header
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Faz logout no Supabase
const response = await fetch(`${SUPABASE_URL}/auth/v1/logout`, {
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
// Logout retorna 204 No Content (sem body)
if (response.status === 204) {
return {
statusCode: 204,
headers,
body: "",
};
}
// Se não for 204, retorna o body da resposta
const data = await response.text();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: data || "{}",
};
} catch (error) {
console.error("Erro no logout:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,95 +0,0 @@
/**
* Netlify Function: Magic Link
* Envia link de autenticação sem senha por email
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
// Constantes da API (protegidas no backend)
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
interface MagicLinkRequest {
email: string;
redirect_url?: string;
}
export const handler: Handler = async (event: HandlerEvent) => {
// CORS headers
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
// Handle preflight
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
// Apenas POST é permitido
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
// Parse body
const body: MagicLinkRequest = JSON.parse(event.body || "{}");
if (!body.email) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Email é obrigatório" }),
};
}
// Faz requisição para API Supabase COM a apikey protegida
const response = await fetch(`${SUPABASE_URL}/auth/v1/otp`, {
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: SUPABASE_ANON_KEY,
},
body: JSON.stringify({
email: body.email,
options: {
emailRedirectTo:
body.redirect_url ||
"https://mediconnectbrasil.netlify.app/auth/callback",
},
}),
});
const data = await response.json();
// Repassa a resposta para o frontend
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("[auth-magic-link] Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro ao enviar magic link",
details: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,87 +0,0 @@
/**
* Netlify Function: Refresh Token
* Renova o access token usando o refresh token
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
interface RefreshTokenRequest {
refresh_token: string;
}
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const body: RefreshTokenRequest = JSON.parse(event.body || "{}");
if (!body.refresh_token) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Refresh token é obrigatório" }),
};
}
// Faz requisição para renovar token no Supabase
const response = await fetch(
`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: SUPABASE_ANON_KEY,
},
body: JSON.stringify({
refresh_token: body.refresh_token,
}),
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro ao renovar token:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,77 +0,0 @@
/**
* Netlify Function: Auth User
* GET /auth/v1/user - Retorna dados do usuário autenticado
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
if (event.httpMethod === "GET") {
const response = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de auth user:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,94 +0,0 @@
/**
* Netlify Function: Delete Avatar
* DELETE /storage/v1/object/avatars/{userId}/avatar
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "DELETE") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
const userId = event.queryStringParameters?.userId;
if (!userId) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "userId é obrigatório" }),
};
}
const response = await fetch(
`${SUPABASE_URL}/storage/v1/object/avatars/${userId}/avatar`,
{
method: "DELETE",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
}
);
// DELETE pode retornar 200 com body vazio
const contentType = response.headers.get("content-type");
const data = contentType?.includes("application/json")
? await response.json()
: {};
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro ao deletar avatar:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,145 +0,0 @@
/**
* Netlify Function: Upload Avatar
* POST /storage/v1/object/avatars/{userId}/avatar
*
* Aceita JSON com base64 para simplificar o upload via Netlify Functions
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Extrai userId do query string
const userId = event.queryStringParameters?.userId;
if (!userId) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "userId é obrigatório" }),
};
}
// Parse JSON body com base64
let fileData: string;
let contentType: string;
try {
const body = JSON.parse(event.body || "{}");
fileData = body.fileData; // base64 string
contentType = body.contentType || "image/jpeg";
if (!fileData) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "fileData (base64) é obrigatório" }),
};
}
} catch {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Body deve ser JSON válido com fileData em base64",
}),
};
}
// Converte base64 para Buffer
const buffer = Buffer.from(fileData, "base64");
// Upload para Supabase Storage
const response = await fetch(
`${SUPABASE_URL}/storage/v1/object/avatars/${userId}/avatar`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": contentType,
"x-upsert": "true", // Sobrescreve se já existir
},
body: buffer,
}
);
const data = await response.json();
if (!response.ok) {
console.error("Erro do Supabase:", data);
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
error: data.error || "Erro ao fazer upload no Supabase",
details: data,
}),
};
}
return {
statusCode: 200,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
message: "Upload realizado com sucesso",
path: data.Key || data.path,
fullPath: data.Key || data.path,
}),
};
} catch (error) {
console.error("Erro no upload do avatar:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,163 +0,0 @@
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return { statusCode: 200, headers, body: "" };
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token n<>o fornecido" }),
};
}
const pathParts = event.path.split("/");
const appointmentId =
pathParts[pathParts.length - 1] !== "consultas"
? pathParts[pathParts.length - 1]
: null;
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/appointments`;
if (appointmentId && appointmentId !== "consultas") {
url += `?id=eq.${appointmentId}&select=*`;
} else if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: { apikey: SUPABASE_ANON_KEY, Authorization: authHeader },
});
let data = await response.json();
if (
appointmentId &&
appointmentId !== "consultas" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
if (!body.patient_id || !body.doctor_id || !body.scheduled_at) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigat<61>rios: patient_id, doctor_id, scheduled_at",
}),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/appointments`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
let data = await response.json();
if (Array.isArray(data) && data.length > 0) data = data[0];
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "PATCH") {
if (!appointmentId || appointmentId === "consultas") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do agendamento <20> obrigat<61>rio" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/appointments?id=eq.${appointmentId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) data = data[0];
return {
statusCode: response.status,
headers: { ...headers, "Content-Type": "application/json" },
body: JSON.stringify(data),
};
}
if (event.httpMethod === "DELETE") {
if (!appointmentId || appointmentId === "consultas") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do agendamento <20> obrigat<61>rio" }),
};
}
const response = await fetch(
`${SUPABASE_URL}/rest/v1/appointments?id=eq.${appointmentId}`,
{
method: "DELETE",
headers: { apikey: SUPABASE_ANON_KEY, Authorization: authHeader },
}
);
return { statusCode: response.status, headers, body: "" };
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({ error: "Erro interno" }),
};
}
};

View File

@ -1,100 +0,0 @@
/**
* Netlify Function: Create Doctor
* POST /create-doctor - Cria registro de médico com validações
* Não cria auth user - apenas registro na tabela doctors
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
const body = JSON.parse(event.body || "{}");
// Validação dos campos obrigatórios
if (
!body.email ||
!body.full_name ||
!body.cpf ||
!body.crm ||
!body.crm_uf
) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: email, full_name, cpf, crm, crm_uf",
}),
};
}
// Chama a Edge Function do Supabase para criar médico
const response = await fetch(`${SUPABASE_URL}/functions/v1/create-doctor`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro na API de create doctor:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,102 +0,0 @@
/**
* Netlify Function: Create Patient
* POST /create-patient - Cria registro de paciente diretamente
* Não cria auth user - apenas registro na tabela patients
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
const body = JSON.parse(event.body || "{}");
// Validação dos campos obrigatórios
if (
!body.full_name ||
!body.cpf ||
!body.email ||
!body.phone_mobile ||
!body.created_by
) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error:
"Campos obrigatórios: full_name, cpf, email, phone_mobile, created_by",
}),
};
}
// Chama REST API do Supabase para criar paciente diretamente
const response = await fetch(`${SUPABASE_URL}/rest/v1/patients`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro na API de create patient:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,223 +0,0 @@
/**
* Netlify Function: Create User With Password
* POST /create-user-with-password - Cria usuário com senha
* Usa Edge Function do Supabase (não Admin API)
* Requer permissão de admin, gestor ou secretaria
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
console.error("[create-user-with-password] Token não fornecido!");
return {
statusCode: 401,
headers,
body: JSON.stringify({
error: "Token de autenticação é obrigatório",
}),
};
}
const body = JSON.parse(event.body || "{}");
console.log(
"[create-user-with-password] Recebido:",
JSON.stringify({ ...body, password: "***" }, null, 2)
);
// Validações
if (!body.email || !body.password || !body.full_name) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: email, password, full_name",
}),
};
}
if (body.password.length < 6) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Senha deve ter no mínimo 6 caracteres",
}),
};
}
// 1. Criar usuário via Edge Function do Supabase
console.log(
"[create-user-with-password] Chamando Edge Function do Supabase..."
);
console.log(
"[create-user-with-password] URL:",
`${SUPABASE_URL}/functions/v1/create-user`
);
console.log("[create-user-with-password] Payload:", {
email: body.email,
has_password: !!body.password,
full_name: body.full_name,
});
const createUserResponse = await fetch(
`${SUPABASE_URL}/functions/v1/create-user`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify({
email: body.email,
password: body.password,
full_name: body.full_name,
phone: body.phone || null,
role: body.role || "user",
}),
}
);
console.log(
"[create-user-with-password] Status da resposta:",
createUserResponse.status
);
console.log(
"[create-user-with-password] Status text:",
createUserResponse.statusText
);
// Sempre tenta ler a resposta como JSON
let responseData;
try {
responseData = await createUserResponse.json();
console.log(
"[create-user-with-password] Resposta JSON:",
JSON.stringify(responseData, null, 2)
);
} catch (error) {
const responseText = await createUserResponse.text();
console.error(
"[create-user-with-password] Resposta não é JSON:",
responseText
);
console.error("[create-user-with-password] Erro ao parsear JSON:", error);
return {
statusCode: 500,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
error: "Erro ao processar resposta do Supabase",
details: responseText,
}),
};
}
if (!createUserResponse.ok) {
console.error(
"[create-user-with-password] Erro ao criar usuário:",
JSON.stringify(responseData, null, 2)
);
return {
statusCode: createUserResponse.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
error:
responseData.msg || responseData.message || "Erro ao criar usuário",
details: responseData,
}),
};
}
// Verificar se a Edge Function retornou sucesso
if (!responseData.success) {
console.error(
"[create-user-with-password] Edge Function retornou erro:",
JSON.stringify(responseData, null, 2)
);
return {
statusCode: 400,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
error: responseData.error || "Erro ao criar usuário",
details: responseData,
}),
};
}
const userData = responseData.user;
console.log(
"[create-user-with-password] Usuário criado com sucesso:",
userData.id
);
console.log(
"[create-user-with-password] Resposta completa:",
JSON.stringify(responseData, null, 2)
);
// A Edge Function já cria o perfil e atribui a role automaticamente
// Retornar sucesso
return {
statusCode: 201,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
success: true,
user: userData,
message: responseData.message || "Usuário criado com sucesso",
}),
};
} catch (error) {
console.error("[create-user-with-password] Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,120 +0,0 @@
/**
* Netlify Function: Create User
* POST /create-user - Cria novo usuário no sistema
* Requer permissão de admin, gestor ou secretaria
* Envia magic link automaticamente para o email
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
// create-user pode ser chamado SEM autenticação (para auto-registro)
// Se houver token, será usado; se não houver, usa apenas anon key
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
console.log(
"[create-user] Recebido body:",
JSON.stringify(body, null, 2)
);
console.log("[create-user] Auth header presente?", !!authHeader);
// Validação dos campos obrigatórios
if (!body.email || !body.full_name) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: email, full_name",
}),
};
}
if (!body.role && (!body.roles || body.roles.length === 0)) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "É necessário fornecer role ou roles",
}),
};
}
// Chama a Edge Function do Supabase para criar usuário
const fetchHeaders: Record<string, string> = {
apikey: SUPABASE_ANON_KEY,
"Content-Type": "application/json",
// Se houver token de usuário autenticado, usa ele; senão usa anon key
Authorization: authHeader || `Bearer ${SUPABASE_ANON_KEY}`,
};
console.log("[create-user] Chamando Supabase com headers:", {
hasAuthHeader: !!authHeader,
hasApikey: !!fetchHeaders.apikey,
authType: authHeader ? "User Token" : "Anon Key",
});
const response = await fetch(`${SUPABASE_URL}/functions/v1/create-user`, {
method: "POST",
headers: fetchHeaders,
body: JSON.stringify(body),
});
const data = await response.json();
console.log("[create-user] Resposta do Supabase:", {
status: response.status,
data: JSON.stringify(data, null, 2),
});
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de create user:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,127 +0,0 @@
/**
* Netlify Function: Delete User (Hard Delete)
* POST /delete-user - Deleta usuário permanentemente
* OPERAÇÃO IRREVERSÍVEL - Use com extremo cuidado!
* Requer permissão de admin ou gestor
* Usa Admin API do Supabase
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_SERVICE_ROLE_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1NDk1NDM2OSwiZXhwIjoyMDcwNTMwMzY5fQ.Dez8PQkV8vWv7VkL_fZe-lY-Xs9P5VptNvRRnhkxoXw";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization, apikey",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({
error: "Token de autenticação é obrigatório",
}),
};
}
const body = JSON.parse(event.body || "{}");
console.log("[delete-user] ATENÇÃO: Tentativa de hard delete:", {
userId: body.userId,
requestedBy: "via Netlify Function",
});
// Validação
if (!body.userId) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "userId é obrigatório",
}),
};
}
// TODO: Aqui deveria verificar se o usuário tem permissão de admin/gestor
// Verificando o token JWT que foi passado
// Deletar usuário via Admin API do Supabase
const response = await fetch(
`${SUPABASE_URL}/auth/v1/admin/users/${body.userId}`,
{
method: "DELETE",
headers: {
apikey: SUPABASE_SERVICE_ROLE_KEY,
Authorization: `Bearer ${SUPABASE_SERVICE_ROLE_KEY}`,
"Content-Type": "application/json",
},
}
);
if (response.ok) {
console.log("[delete-user] Usuário deletado com sucesso:", body.userId);
return {
statusCode: 200,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
success: true,
message: "Usuário deletado permanentemente",
userId: body.userId,
}),
};
}
const errorData = await response.json();
console.error("[delete-user] Erro ao deletar:", errorData);
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
error: errorData.msg || errorData.message || "Erro ao deletar usuário",
details: errorData,
}),
};
} catch (error) {
console.error("[delete-user] Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,217 +0,0 @@
/**
* Netlify Function: doctor-availability
*
* Proxy para operações de disponibilidade dos médicos
* GET: Lista disponibilidades
* POST: Criar disponibilidade
* PATCH: Atualizar disponibilidade
* DELETE: Deletar disponibilidade
*/
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_API_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
const SUPABASE_SERVICE_ROLE_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTc1NDk1NDM2OSwiZXhwIjoyMDcwNTMwMzY5fQ.Dez8PQkV8vWv7VkL_fZe-lY-Xs9P5VptNvRRnhkxoXw";
export default async (req: Request) => {
// Permitir CORS
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
try {
const url = new URL(req.url);
const authHeader = req.headers.get("Authorization");
// Extrair ID do path se existir
const pathParts = url.pathname.split("/");
const availabilityId = pathParts[pathParts.length - 1];
// GET: Listar disponibilidades
if (req.method === "GET") {
const select = url.searchParams.get("select") || "*";
const doctor_id = url.searchParams.get("doctor_id");
const active = url.searchParams.get("active");
const queryParams = new URLSearchParams();
queryParams.append("select", select);
if (doctor_id) queryParams.append("doctor_id", `eq.${doctor_id}`);
if (active !== null) queryParams.append("active", `eq.${active}`);
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability?${queryParams}`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "GET",
headers,
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// POST: Criar disponibilidade
if (req.method === "POST") {
const body = await req.json();
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability`;
// Usa SERVICE ROLE KEY para ignorar políticas RLS
const headers: HeadersInit = {
apikey: SUPABASE_SERVICE_ROLE_KEY,
Authorization: `Bearer ${SUPABASE_SERVICE_ROLE_KEY}`,
"Content-Type": "application/json",
Prefer: "return=representation",
};
const response = await fetch(supabaseUrl, {
method: "POST",
headers,
body: JSON.stringify(body),
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// PATCH: Atualizar disponibilidade
if (req.method === "PATCH") {
if (!availabilityId || availabilityId === "doctor-availability") {
return new Response(
JSON.stringify({ error: "Availability ID is required" }),
{
status: 400,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
}
const body = await req.json();
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability?id=eq.${availabilityId}`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
Prefer: "return=representation",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "PATCH",
headers,
body: JSON.stringify(body),
});
const data = await response.json();
const result = Array.isArray(data) && data.length > 0 ? data[0] : data;
return new Response(JSON.stringify(result), {
status: response.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// DELETE: Deletar disponibilidade
if (req.method === "DELETE") {
if (!availabilityId || availabilityId === "doctor-availability") {
return new Response(
JSON.stringify({ error: "Availability ID is required" }),
{
status: 400,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
}
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_availability?id=eq.${availabilityId}`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "DELETE",
headers,
});
return new Response(null, {
status: response.status,
headers: {
"Access-Control-Allow-Origin": "*",
},
});
}
// Método não suportado
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
} catch (error) {
console.error("Error in doctor-availability function:", error);
return new Response(
JSON.stringify({
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
}
};

View File

@ -1,169 +0,0 @@
/**
* Netlify Function: doctor-exceptions
*
* Proxy para operações de exceções na agenda dos médicos
* GET: Lista exceções
* POST: Criar exceção
* DELETE: Deletar exceção
*/
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_API_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export default async (req: Request) => {
// Permitir CORS
if (req.method === "OPTIONS") {
return new Response(null, {
status: 204,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
});
}
try {
const url = new URL(req.url);
const authHeader = req.headers.get("Authorization");
// Extrair ID do path se existir
const pathParts = url.pathname.split("/");
const exceptionId = pathParts[pathParts.length - 1];
// GET: Listar exceções
if (req.method === "GET") {
const select = url.searchParams.get("select") || "*";
const doctor_id = url.searchParams.get("doctor_id");
const date = url.searchParams.get("date");
const queryParams = new URLSearchParams();
queryParams.append("select", select);
if (doctor_id) queryParams.append("doctor_id", `eq.${doctor_id}`);
if (date) queryParams.append("date", `eq.${date}`);
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_exceptions?${queryParams}`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "GET",
headers,
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// POST: Criar exceção
if (req.method === "POST") {
const body = await req.json();
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_exceptions`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
Prefer: "return=representation",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "POST",
headers,
body: JSON.stringify(body),
});
const data = await response.json();
return new Response(JSON.stringify(data), {
status: response.status,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
}
// DELETE: Deletar exceção
if (req.method === "DELETE") {
if (!exceptionId || exceptionId === "doctor-exceptions") {
return new Response(
JSON.stringify({ error: "Exception ID is required" }),
{
status: 400,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
}
const supabaseUrl = `${SUPABASE_URL}/rest/v1/doctor_exceptions?id=eq.${exceptionId}`;
const headers: HeadersInit = {
apikey: SUPABASE_API_KEY,
"Content-Type": "application/json",
};
if (authHeader) {
headers["Authorization"] = authHeader;
}
const response = await fetch(supabaseUrl, {
method: "DELETE",
headers,
});
return new Response(null, {
status: response.status,
headers: {
"Access-Control-Allow-Origin": "*",
},
});
}
// Método não suportado
return new Response(JSON.stringify({ error: "Method not allowed" }), {
status: 405,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
});
} catch (error) {
console.error("Error in doctor-exceptions function:", error);
return new Response(
JSON.stringify({
error: "Internal server error",
details: error instanceof Error ? error.message : "Unknown error",
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*",
},
}
);
}
};

View File

@ -1,237 +0,0 @@
/**
* Netlify Function: Doctors CRUD
* GET /rest/v1/doctors - Lista médicos
* GET /rest/v1/doctors/{id} - Busca por ID
* POST /rest/v1/doctors - Cria médico
* PATCH /rest/v1/doctors/{id} - Atualiza médico
* DELETE /rest/v1/doctors/{id} - Deleta médico
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Extrai ID da URL se houver (doctors/123 ou doctors?id=123)
const pathParts = event.path.split("/");
const doctorId =
pathParts[pathParts.length - 1] !== "doctors"
? pathParts[pathParts.length - 1]
: null;
// GET - Listar ou buscar por ID
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/doctors`;
if (doctorId && doctorId !== "doctors") {
// Buscar por ID específico
url += `?id=eq.${doctorId}&select=*`;
} else if (event.queryStringParameters) {
// Adiciona filtros da query string
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
// Adiciona select=* se não tiver
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
let data = await response.json();
// Se buscar por ID, retorna o objeto diretamente (não array)
if (
doctorId &&
doctorId !== "doctors" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// POST - Criar médico
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
if (
!body.crm ||
!body.crm_uf ||
!body.full_name ||
!body.cpf ||
!body.email
) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: crm, crm_uf, full_name, cpf, email",
}),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/doctors`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
let data = await response.json();
// Supabase retorna array, pega o primeiro
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// PATCH - Atualizar médico
if (event.httpMethod === "PATCH") {
if (!doctorId || doctorId === "doctors") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do médico é obrigatório" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/doctors?id=eq.${doctorId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// DELETE - Deletar médico
if (event.httpMethod === "DELETE") {
if (!doctorId || doctorId === "doctors") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do médico é obrigatório" }),
};
}
const response = await fetch(
`${SUPABASE_URL}/rest/v1/doctors?id=eq.${doctorId}`,
{
method: "DELETE",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
}
);
return {
statusCode: response.status,
headers,
body: "",
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de médicos:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,95 +0,0 @@
/**
* Netlify Function: Get Available Slots
* POST /functions/v1/get-available-slots - Busca horários disponíveis
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
// Validação dos campos obrigatórios
if (!body.doctor_id || !body.start_date || !body.end_date) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: doctor_id, start_date, end_date",
}),
};
}
const response = await fetch(
`${SUPABASE_URL}/functions/v1/get-available-slots`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de available slots:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,226 +0,0 @@
/**
* Netlify Function: Patients CRUD
* GET /rest/v1/patients - Lista pacientes
* GET /rest/v1/patients/{id} - Busca por ID
* POST /rest/v1/patients - Cria paciente
* PATCH /rest/v1/patients/{id} - Atualiza paciente
* DELETE /rest/v1/patients/{id} - Deleta paciente
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Extrai ID da URL se houver
const pathParts = event.path.split("/");
const patientId =
pathParts[pathParts.length - 1] !== "patients"
? pathParts[pathParts.length - 1]
: null;
// GET - Listar ou buscar por ID
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/patients`;
if (patientId && patientId !== "patients") {
url += `?id=eq.${patientId}&select=*`;
} else if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
let data = await response.json();
if (
patientId &&
patientId !== "patients" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// POST - Criar paciente
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
if (!body.full_name || !body.cpf || !body.email || !body.phone_mobile) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: full_name, cpf, email, phone_mobile",
}),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/patients`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// PATCH - Atualizar paciente
if (event.httpMethod === "PATCH") {
if (!patientId || patientId === "patients") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do paciente é obrigatório" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/patients?id=eq.${patientId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// DELETE - Deletar paciente
if (event.httpMethod === "DELETE") {
if (!patientId || patientId === "patients") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do paciente é obrigatório" }),
};
}
const response = await fetch(
`${SUPABASE_URL}/rest/v1/patients?id=eq.${patientId}`,
{
method: "DELETE",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
}
);
return {
statusCode: response.status,
headers,
body: "",
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de pacientes:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,155 +0,0 @@
/**
* Netlify Function: Profiles
* GET /rest/v1/profiles - Lista perfis
* GET /rest/v1/profiles/{id} - Busca por ID
* PATCH /rest/v1/profiles/{id} - Atualiza avatar_url
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, PATCH, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Extrai ID da URL se houver
const pathParts = event.path.split("/");
const profileId =
pathParts[pathParts.length - 1] !== "profiles"
? pathParts[pathParts.length - 1]
: null;
// GET - Listar ou buscar por ID
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/profiles`;
if (profileId && profileId !== "profiles") {
url += `?id=eq.${profileId}&select=*`;
} else if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
let data = await response.json();
if (
profileId &&
profileId !== "profiles" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// PATCH - Atualizar avatar_url
if (event.httpMethod === "PATCH") {
if (!profileId || profileId === "profiles") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do perfil é obrigatório" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/profiles?id=eq.${profileId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de perfis:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,97 +0,0 @@
/**
* Netlify Function: Register Patient (Public)
* POST /register-patient - Registro público de paciente
* Não requer autenticação - função pública
* Validações rigorosas (CPF, rate limiting, rollback)
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
console.log(
"[register-patient] Recebido body:",
JSON.stringify(body, null, 2)
);
// Validação dos campos obrigatórios
if (!body.email || !body.full_name || !body.cpf || !body.phone_mobile) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: email, full_name, cpf, phone_mobile",
}),
};
}
// Chama a Edge Function pública do Supabase
const response = await fetch(
`${SUPABASE_URL}/functions/v1/register-patient`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: `Bearer ${SUPABASE_ANON_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}
);
const data = await response.json();
console.log("[register-patient] Resposta do Supabase:", {
status: response.status,
data: JSON.stringify(data, null, 2),
});
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("[register-patient] Erro na API:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno do servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,197 +0,0 @@
/**
* Netlify Function: Reports
* GET /rest/v1/reports - Lista relatórios
* GET /rest/v1/reports/{id} - Busca por ID
* POST /rest/v1/reports - Cria relatório
* PATCH /rest/v1/reports/{id} - Atualiza relatório
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Extrai ID da URL se houver
const pathParts = event.path.split("/");
const reportId =
pathParts[pathParts.length - 1] !== "reports"
? pathParts[pathParts.length - 1]
: null;
// GET - Listar ou buscar por ID
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/reports`;
if (reportId && reportId !== "reports") {
url += `?id=eq.${reportId}&select=*`;
} else if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
url += `?${params.toString()}`;
if (!params.has("select")) {
url += url.includes("?") ? "&select=*" : "?select=*";
}
} else {
url += "?select=*";
}
const response = await fetch(url, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
let data = await response.json();
if (
reportId &&
reportId !== "reports" &&
Array.isArray(data) &&
data.length > 0
) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// POST - Criar relatório
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
if (!body.patient_id) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campo obrigatório: patient_id",
}),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/reports`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
});
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
// PATCH - Atualizar relatório
if (event.httpMethod === "PATCH") {
if (!reportId || reportId === "reports") {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "ID do relatório é obrigatório" }),
};
}
const body = JSON.parse(event.body || "{}");
const response = await fetch(
`${SUPABASE_URL}/rest/v1/reports?id=eq.${reportId}`,
{
method: "PATCH",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify(body),
}
);
let data = await response.json();
if (Array.isArray(data) && data.length > 0) {
data = data[0];
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de relatórios:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,116 +0,0 @@
/**
* Netlify Function: Request Password Reset
* POST /request-password-reset - Solicita reset de senha via email (público)
* Não requer autenticação - endpoint público
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
interface PasswordResetRequest {
email: string;
redirect_url?: string;
}
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const body: PasswordResetRequest = JSON.parse(event.body || "{}");
console.log("[request-password-reset] Recebido:", {
email: body.email,
hasRedirectUrl: !!body.redirect_url,
});
if (!body.email) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Email é obrigatório" }),
};
}
// Chama a API do Supabase para enviar email de reset
const response = await fetch(`${SUPABASE_URL}/auth/v1/recover`, {
method: "POST",
headers: {
"Content-Type": "application/json",
apikey: SUPABASE_ANON_KEY,
},
body: JSON.stringify({
email: body.email,
options: {
redirectTo:
body.redirect_url ||
"https://mediconnectbrasil.netlify.app/reset-password",
},
}),
});
const data = await response.json();
console.log("[request-password-reset] Resposta Supabase:", {
status: response.status,
data,
});
// Supabase sempre retorna 200 mesmo se o email não existir (por segurança)
if (response.ok) {
return {
statusCode: 200,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
success: true,
message:
"Email de reset de senha enviado com sucesso. Verifique sua caixa de entrada.",
}),
};
}
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("[request-password-reset] Erro:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro ao solicitar reset de senha",
details: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,93 +0,0 @@
/**
* Netlify Function: Send SMS
* POST /functions/v1/send-sms - Envia SMS via Twilio
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
if (event.httpMethod === "POST") {
const body = JSON.parse(event.body || "{}");
// Validação dos campos obrigatórios
if (!body.phone_number || !body.message) {
return {
statusCode: 400,
headers,
body: JSON.stringify({
error: "Campos obrigatórios: phone_number, message",
}),
};
}
// Chama a função Supabase de enviar SMS
const response = await fetch(`${SUPABASE_URL}/functions/v1/send-sms`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de SMS:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,93 +0,0 @@
/**
* Netlify Function: User Info By ID
* POST /user-info-by-id - Retorna dados de usuário específico (apenas admin/gestor)
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
if (event.httpMethod !== "POST") {
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
const body = JSON.parse(event.body || "{}");
if (!body.user_id) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "Campo obrigatório: user_id" }),
};
}
// Chama a Edge Function do Supabase
const response = await fetch(
`${SUPABASE_URL}/functions/v1/user-info-by-id`,
{
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
}
);
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
} catch (error) {
console.error("Erro na API de user-info-by-id:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,81 +0,0 @@
/**
* Netlify Function: User Info
* GET /functions/v1/user-info - Retorna informações completas do usuário autenticado
* Inclui: user, profile, roles e permissions calculadas
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
// Aceita tanto POST quanto GET para compatibilidade
if (event.httpMethod === "POST" || event.httpMethod === "GET") {
// Chama a Edge Function do Supabase (POST conforme doc 21/10/2025)
const response = await fetch(`${SUPABASE_URL}/functions/v1/user-info`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
},
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de user-info:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1,161 +0,0 @@
/**
* Netlify Function: User Roles
* GET /rest/v1/user_roles - Lista roles de usuários
* POST /rest/v1/user_roles - Adiciona role a um usuário
* DELETE /rest/v1/user_roles - Remove role de um usuário
*/
import type { Handler, HandlerEvent } from "@netlify/functions";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
export const handler: Handler = async (event: HandlerEvent) => {
const headers = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
"Access-Control-Allow-Methods": "GET, POST, DELETE, OPTIONS",
};
if (event.httpMethod === "OPTIONS") {
return {
statusCode: 200,
headers,
body: "",
};
}
try {
const authHeader =
event.headers.authorization || event.headers.Authorization;
if (!authHeader) {
return {
statusCode: 401,
headers,
body: JSON.stringify({ error: "Token não fornecido" }),
};
}
if (event.httpMethod === "GET") {
let url = `${SUPABASE_URL}/rest/v1/user_roles?select=*`;
if (event.queryStringParameters) {
const params = new URLSearchParams(
event.queryStringParameters as Record<string, string>
);
const paramsStr = params.toString();
if (paramsStr) {
url += `&${paramsStr}`;
}
}
const response = await fetch(url, {
method: "GET",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
if (event.httpMethod === "POST") {
// Adicionar nova role para um usuário
const body = JSON.parse(event.body || "{}");
if (!body.user_id || !body.role) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "user_id e role são obrigatórios" }),
};
}
const response = await fetch(`${SUPABASE_URL}/rest/v1/user_roles`, {
method: "POST",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
"Content-Type": "application/json",
Prefer: "return=representation",
},
body: JSON.stringify({
user_id: body.user_id,
role: body.role,
}),
});
const data = await response.json();
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(data),
};
}
if (event.httpMethod === "DELETE") {
// Remover role de um usuário
const params = event.queryStringParameters;
if (!params?.user_id || !params?.role) {
return {
statusCode: 400,
headers,
body: JSON.stringify({ error: "user_id e role são obrigatórios" }),
};
}
const url = `${SUPABASE_URL}/rest/v1/user_roles?user_id=eq.${params.user_id}&role=eq.${params.role}`;
const response = await fetch(url, {
method: "DELETE",
headers: {
apikey: SUPABASE_ANON_KEY,
Authorization: authHeader,
},
});
return {
statusCode: response.status,
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({ success: true }),
};
}
return {
statusCode: 405,
headers,
body: JSON.stringify({ error: "Method Not Allowed" }),
};
} catch (error) {
console.error("Erro na API de user roles:", error);
return {
statusCode: 500,
headers,
body: JSON.stringify({
error: "Erro interno no servidor",
message: error instanceof Error ? error.message : "Erro desconhecido",
}),
};
}
};

View File

@ -1 +0,0 @@
/* /index.html 200

Binary file not shown.

After

Width:  |  Height:  |  Size: 472 KiB

View File

@ -0,0 +1,134 @@
import axios from "axios";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
let ACCESS_TOKEN = "";
// 1. Login como admin
async function login() {
console.log("\n🔐 Fazendo login como admin...");
try {
const response = await axios.post(
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
{
email: "riseup@popcode.com.br",
password: "riseup",
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
ACCESS_TOKEN = response.data.access_token;
console.log("✅ Login realizado com sucesso!");
console.log(`📧 Email: ${response.data.user.email}`);
console.log(`🆔 User ID: ${response.data.user.id}`);
return response.data;
} catch (error) {
console.error("❌ Erro no login:", error.response?.data || error.message);
process.exit(1);
}
}
// 2. Listar usuários
async function listUsers() {
console.log("\n📋 Listando usuários...\n");
try {
const response = await axios.get(
`${SUPABASE_URL}/rest/v1/profiles?select=id,full_name,email`,
{
headers: {
apikey: ANON_KEY,
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
}
);
console.log(`${response.data.length} usuários encontrados:\n`);
response.data.forEach((user, index) => {
console.log(`${index + 1}. ${user.full_name || "Sem nome"}`);
console.log(` 📧 Email: ${user.email}`);
console.log(` 🆔 ID: ${user.id}\n`);
});
return response.data;
} catch (error) {
console.error(
"❌ Erro ao listar usuários:",
error.response?.data || error.message
);
return [];
}
}
// 3. Deletar usuário
async function deleteUser(userId, userName) {
console.log(`\n🗑️ Deletando usuário: ${userName} (${userId})...`);
try {
const response = await axios.post(
`${SUPABASE_URL}/functions/v1/delete-user`,
{ userId },
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
}
);
console.log(`${userName} deletado com sucesso!`);
return response.data;
} catch (error) {
console.error(
`❌ Erro ao deletar ${userName}:`,
error.response?.data || error.message
);
}
}
// Script principal
async function main() {
console.log("🧹 Iniciando limpeza de usuários de teste...");
// 1. Login
await login();
// 2. Listar usuários atuais
const users = await listUsers();
// 3. Lista de emails para deletar (apenas os que o assistente criou)
const testEmails = [
"admin@mediconnect.com",
"secretaria@mediconnect.com",
"dr.medico@mediconnect.com",
"fernando.pirichowski@souunit.com.br",
];
// 4. Deletar usuários de teste
let deletedCount = 0;
for (const user of users) {
if (testEmails.includes(user.email)) {
await deleteUser(user.id, user.full_name || user.email);
deletedCount++;
// Aguardar 1 segundo entre deleções
await new Promise((resolve) => setTimeout(resolve, 1000));
}
}
console.log(
`\n✅ Limpeza concluída! ${deletedCount} usuários de teste deletados.`
);
// 5. Listar usuários finais
console.log("\n📊 Usuários restantes:");
await listUsers();
}
main().catch(console.error);

View File

@ -0,0 +1,275 @@
import axios from "axios";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
// Credenciais do admin
const ADMIN_EMAIL = "riseup@popcode.com.br";
const ADMIN_PASSWORD = "riseup";
let ACCESS_TOKEN = "";
// 1. Fazer login como admin
async function login() {
console.log("\n🔐 Fazendo login como admin...");
try {
const response = await axios.post(
`${SUPABASE_URL}/auth/v1/token?grant_type=password`,
{
email: ADMIN_EMAIL,
password: ADMIN_PASSWORD,
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
ACCESS_TOKEN = response.data.access_token;
console.log("✅ Login realizado com sucesso!");
console.log("📧 Email:", response.data.user.email);
console.log("🆔 User ID:", response.data.user.id);
return response.data;
} catch (error) {
console.error("❌ Erro no login:", error.response?.data || error.message);
process.exit(1);
}
}
// 2. Listar todos os usuários (via profiles - simplificado)
async function listUsers() {
console.log("\n📋 Listando usuários...");
try {
const response = await axios.get(
`${SUPABASE_URL}/rest/v1/profiles?select=id,full_name,email`,
{
headers: {
apikey: ANON_KEY,
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
}
);
console.log(`\n${response.data.length} usuários encontrados:\n`);
response.data.forEach((user, index) => {
console.log(`${index + 1}. ${user.full_name || "Sem nome"}`);
console.log(` 📧 Email: ${user.email || "Sem email"}`);
console.log(` 🆔 ID: ${user.id}`);
console.log("");
});
return response.data;
} catch (error) {
console.error(
"❌ Erro ao listar usuários:",
error.response?.data || error.message
);
return [];
}
}
// 3. Deletar usuário (Edge Function)
async function deleteUser(userId, userName) {
console.log(`\n🗑️ Deletando usuário: ${userName} (${userId})...`);
try {
const response = await axios.post(
`${SUPABASE_URL}/functions/v1/delete-user`,
{ userId },
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
}
);
console.log(`${userName} deletado com sucesso!`);
return response.data;
} catch (error) {
console.error(
`❌ Erro ao deletar ${userName}:`,
error.response?.data || error.message
);
}
}
// 4. Criar novo usuário com Edge Function
async function createUserWithPassword(email, password, fullName, role) {
console.log(`\n Criando usuário: ${fullName} (${role})...`);
try {
const response = await axios.post(
`${SUPABASE_URL}/functions/v1/create-user`,
{
email,
password,
full_name: fullName,
role,
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
}
);
console.log(`${fullName} criado com sucesso!`);
console.log(` 📧 Email: ${email}`);
console.log(` 🔑 Senha: ${password}`);
console.log(` 👤 Role: ${role}`);
return response.data;
} catch (error) {
console.error(
`❌ Erro ao criar ${fullName}:`,
error.response?.data || error.message
);
}
}
// 5. Criar médico com Edge Function
async function createDoctor(
email,
password,
fullName,
especialidade,
crm,
crmUf,
cpf
) {
console.log(`\n Criando médico: ${fullName}...`);
try {
const response = await axios.post(
`${SUPABASE_URL}/functions/v1/create-doctor`,
{
email,
password,
full_name: fullName,
cpf,
especialidade,
crm,
crm_uf: crmUf,
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
Authorization: `Bearer ${ACCESS_TOKEN}`,
},
}
);
console.log(`${fullName} criado com sucesso!`);
console.log(` 📧 Email: ${email}`);
console.log(` 🔑 Senha: ${password}`);
console.log(` 🆔 CPF: ${cpf}`);
console.log(` 🩺 Especialidade: ${especialidade}`);
console.log(` 📋 CRM: ${crm}-${crmUf}`);
return response.data;
} catch (error) {
console.error(
`❌ Erro ao criar ${fullName}:`,
error.response?.data || error.message
);
}
}
// Script principal
async function main() {
console.log("🚀 Iniciando gerenciamento de usuários...");
// 1. Login
await login();
// 2. Listar usuários atuais
const users = await listUsers();
// 3. Encontrar e deletar admin e médico específicos (por email)
const adminToDelete = users.find((u) => u.email === "admin@mediconnect.com");
const secretariaToDelete = users.find(
(u) => u.email === "secretaria@mediconnect.com"
);
const medicoToDelete = users.find(
(u) =>
u.email === "medico@mediconnect.com" ||
u.email === "dr.medico@mediconnect.com"
);
if (adminToDelete) {
await deleteUser(
adminToDelete.id,
adminToDelete.full_name || adminToDelete.email
);
} else {
console.log("\n⚠ Nenhum admin adicional encontrado para deletar");
}
if (secretariaToDelete) {
await deleteUser(
secretariaToDelete.id,
secretariaToDelete.full_name || secretariaToDelete.email
);
} else {
console.log("\n⚠ Nenhuma secretária encontrada para deletar");
}
if (medicoToDelete) {
await deleteUser(
medicoToDelete.id,
medicoToDelete.full_name || medicoToDelete.email
);
} else {
console.log("\n⚠ Nenhum médico encontrado para deletar");
}
// 4. Aguardar um pouco
console.log("\n⏳ Aguardando 2 segundos...");
await new Promise((resolve) => setTimeout(resolve, 2000));
// 5. Criar novos usuários
await createUserWithPassword(
"admin@mediconnect.com",
"admin123",
"Administrador Sistema",
"admin"
);
await createUserWithPassword(
"secretaria@mediconnect.com",
"secretaria123",
"Secretária Sistema",
"secretaria"
);
await createDoctor(
"dr.medico@mediconnect.com",
"medico123",
"Dr. João Silva",
"Cardiologia",
"12345",
"SP",
"12345678900"
);
// 6. Listar usuários finais
console.log("\n📊 Estado final dos usuários:");
await listUsers();
console.log("\n✅ Processo concluído!");
console.log("\n📝 Credenciais dos novos usuários:");
console.log(" 👨‍💼 Admin: admin@mediconnect.com / admin123");
console.log(" <20>💼 Secretária: secretaria@mediconnect.com / secretaria123");
console.log(" <20>👨 Médico: dr.medico@mediconnect.com / medico123");
console.log(" 🆔 CPF: 12345678900");
console.log(" 🩺 Especialidade: Cardiologia");
console.log(" 📋 CRM: 12345-SP");
}
// Executar
main().catch(console.error);

View File

@ -0,0 +1,86 @@
const axios = require('axios');
const ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
const BASE_URL = 'https://yuanqfswhberkoevtmfr.supabase.co';
(async () => {
try {
console.log('🔐 Fazendo login como admin...');
const loginRes = await axios.post(`${BASE_URL}/auth/v1/token?grant_type=password`, {
email: 'riseup@popcode.com.br',
password: 'riseup'
}, {
headers: {
'Content-Type': 'application/json',
'apikey': ANON_KEY
}
});
console.log('✅ Login admin bem-sucedido!\n');
const token = loginRes.data.access_token;
console.log('🔍 Buscando usuário fernando na tabela profiles...');
const profilesRes = await axios.get(`${BASE_URL}/rest/v1/profiles?select=*`, {
headers: {
'apikey': ANON_KEY,
'Authorization': `Bearer ${token}`
}
});
console.log(`📊 Total de profiles: ${profilesRes.data.length}\n`);
const fernandoProfile = profilesRes.data.find(u =>
u.email && (
u.email.toLowerCase().includes('fernando') ||
u.full_name?.toLowerCase().includes('fernando')
)
);
if (fernandoProfile) {
console.log('✅ Fernando encontrado na tabela profiles:');
console.log(JSON.stringify(fernandoProfile, null, 2));
} else {
console.log('❌ Fernando NÃO encontrado na tabela profiles\n');
}
// Buscar nos pacientes também
console.log('\n🔍 Buscando fernando na tabela patients...');
const patientsRes = await axios.get(`${BASE_URL}/rest/v1/patients?select=*`, {
headers: {
'apikey': ANON_KEY,
'Authorization': `Bearer ${token}`
}
});
console.log(`📊 Total de patients: ${patientsRes.data.length}\n`);
const fernandoPatient = patientsRes.data.find(p =>
p.email && (
p.email.toLowerCase().includes('fernando') ||
p.full_name?.toLowerCase().includes('fernando')
)
);
if (fernandoPatient) {
console.log('✅ Fernando encontrado na tabela patients:');
console.log(JSON.stringify(fernandoPatient, null, 2));
} else {
console.log('❌ Fernando NÃO encontrado na tabela patients\n');
}
// Listar alguns emails para referência
if (!fernandoProfile && !fernandoPatient) {
console.log('\n📧 Alguns emails cadastrados nos profiles:');
profilesRes.data.slice(0, 10).forEach((u, i) => {
if (u.email) console.log(` ${i+1}. ${u.email} - ${u.full_name || 'sem nome'}`);
});
}
} catch (err) {
console.error('❌ Erro:', err.response?.data || err.message);
if (err.response) {
console.error('Status:', err.response.status);
console.error('Headers:', err.response.headers);
}
}
})();

View File

@ -26,6 +26,7 @@ import PerfilMedico from "./pages/PerfilMedico";
import PerfilPaciente from "./pages/PerfilPaciente";
import ClearCache from "./pages/ClearCache";
import AuthCallback from "./pages/AuthCallback";
import ResetPassword from "./pages/ResetPassword";
function App() {
return (
@ -48,6 +49,7 @@ function App() {
<Route path="/" element={<Home />} />
<Route path="/clear-cache" element={<ClearCache />} />
<Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/reset-password" element={<ResetPassword />} />
<Route path="/paciente" element={<LoginPaciente />} />
<Route path="/login-secretaria" element={<LoginSecretaria />} />
<Route path="/login-medico" element={<LoginMedico />} />

View File

@ -0,0 +1,277 @@
import React, { useState, useRef, useEffect } from "react";
import { MessageCircle, X, Send } from "lucide-react";
interface Message {
id: string;
text: string;
sender: "user" | "bot";
timestamp: Date;
}
interface ChatbotProps {
className?: string;
}
const Chatbot: React.FC<ChatbotProps> = ({ className = "" }) => {
const [isOpen, setIsOpen] = useState(false);
const [messages, setMessages] = useState<Message[]>([
{
id: "welcome",
text: "Olá! Sou o assistente virtual do MediConnect. Como posso ajudá-lo hoje?",
sender: "bot",
timestamp: new Date(),
},
]);
const [inputValue, setInputValue] = useState("");
const [isTyping, setIsTyping] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const quickReplies = [
"Como agendar uma consulta?",
"Como cancelar agendamento?",
"Esqueci minha senha",
"Suporte técnico",
];
const getBotResponse = (userMessage: string): string => {
const message = userMessage.toLowerCase();
// Respostas baseadas em palavras-chave
if (message.includes("agendar") || message.includes("marcar")) {
return "Para agendar uma consulta:\n\n1. Acesse 'Agendar Consulta' no menu\n2. Selecione o médico desejado\n3. Escolha data e horário disponível\n4. Confirme o agendamento\n\nVocê receberá uma confirmação por e-mail!";
}
if (message.includes("cancelar") || message.includes("remarcar")) {
return "Para cancelar ou remarcar uma consulta:\n\n1. Vá em 'Minhas Consultas'\n2. Localize a consulta\n3. Clique em 'Cancelar' ou 'Remarcar'\n\nRecomendamos fazer isso com 24h de antecedência para evitar taxas.";
}
if (message.includes("senha") || message.includes("login")) {
return "Para recuperar sua senha:\n\n1. Clique em 'Esqueceu a senha?' na tela de login\n2. Insira seu e-mail cadastrado\n3. Você receberá um link para redefinir a senha\n\nSe não receber o e-mail, verifique sua caixa de spam.";
}
if (message.includes("pagamento") || message.includes("pagar")) {
return "Aceitamos as seguintes formas de pagamento:\n\n• Cartão de crédito (parcelamento em até 3x)\n• Cartão de débito\n• PIX\n• Boleto bancário\n\nTodos os pagamentos são processados com segurança.";
}
if (message.includes("teleconsulta") || message.includes("online")) {
return "Para realizar uma teleconsulta:\n\n1. Acesse 'Minhas Consultas' no horário agendado\n2. Clique em 'Iniciar Consulta Online'\n3. Permita acesso à câmera e microfone\n\nCertifique-se de ter uma boa conexão de internet!";
}
if (message.includes("histórico") || message.includes("prontuário")) {
return "Seu histórico médico pode ser acessado em:\n\n• 'Meu Perfil' > 'Histórico Médico'\n• 'Minhas Consultas' (consultas anteriores)\n\nVocê pode fazer download de relatórios e receitas quando necessário.";
}
if (
message.includes("suporte") ||
message.includes("ajuda") ||
message.includes("atendimento")
) {
return "Nossa equipe de suporte está disponível:\n\n📞 Telefone: 0800-123-4567\n📧 E-mail: suporte@mediconnect.com.br\n⏰ Horário: Segunda a Sexta, 8h às 18h\n\nVocê também pode acessar nossa Central de Ajuda completa no menu.";
}
if (message.includes("obrigad") || message.includes("valeu")) {
return "Por nada! Estou sempre aqui para ajudar. Se tiver mais dúvidas, é só chamar! 😊";
}
if (
message.includes("oi") ||
message.includes("olá") ||
message.includes("hello")
) {
return "Olá! Como posso ajudá-lo hoje? Você pode perguntar sobre agendamentos, consultas, pagamentos ou qualquer dúvida sobre o MediConnect.";
}
// Resposta padrão
return "Desculpe, não entendi sua pergunta. Você pode:\n\n• Perguntar sobre agendamentos\n• Consultar formas de pagamento\n• Saber sobre teleconsultas\n• Acessar histórico médico\n• Falar com suporte\n\nOu visite nossa Central de Ajuda para mais informações!";
};
const handleSend = () => {
if (!inputValue.trim()) return;
// Adiciona mensagem do usuário
const userMessage: Message = {
id: Date.now().toString(),
text: inputValue,
sender: "user",
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setInputValue("");
// Simula digitação do bot
setIsTyping(true);
setTimeout(() => {
const botResponse: Message = {
id: (Date.now() + 1).toString(),
text: getBotResponse(inputValue),
sender: "bot",
timestamp: new Date(),
};
setMessages((prev) => [...prev, botResponse]);
setIsTyping(false);
}, 1000);
};
const handleQuickReply = (reply: string) => {
setInputValue(reply);
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className={`fixed bottom-6 right-6 z-50 ${className}`}>
{/* Floating Button */}
{!isOpen && (
<button
onClick={() => setIsOpen(true)}
className="bg-blue-600 hover:bg-blue-700 text-white rounded-full p-4 shadow-lg transition-all hover:scale-110 flex items-center gap-2"
aria-label="Abrir chat de ajuda"
>
<MessageCircle className="w-6 h-6" />
<span className="font-medium">Precisa de ajuda?</span>
</button>
)}
{/* Chat Window */}
{isOpen && (
<div className="bg-white rounded-lg shadow-2xl w-96 h-[600px] flex flex-col">
{/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 text-white p-4 rounded-t-lg flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="bg-white/20 rounded-full p-2">
<MessageCircle className="w-5 h-5" />
</div>
<div>
<h3 className="font-semibold">Assistente MediConnect</h3>
<p className="text-xs text-blue-100">
Online Responde em segundos
</p>
</div>
</div>
<button
onClick={() => setIsOpen(false)}
className="hover:bg-white/20 rounded-full p-1 transition"
aria-label="Fechar chat"
>
<X className="w-5 h-5" />
</button>
</div>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
{messages.map((message) => (
<div
key={message.id}
className={`flex ${
message.sender === "user" ? "justify-end" : "justify-start"
}`}
>
<div
className={`max-w-[80%] rounded-lg p-3 ${
message.sender === "user"
? "bg-blue-600 text-white"
: "bg-white text-gray-800 shadow"
}`}
>
<p className="text-sm whitespace-pre-line">{message.text}</p>
<p
className={`text-xs mt-1 ${
message.sender === "user"
? "text-blue-100"
: "text-gray-400"
}`}
>
{message.timestamp.toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
})}
</p>
</div>
</div>
))}
{isTyping && (
<div className="flex justify-start">
<div className="bg-white text-gray-800 shadow rounded-lg p-3">
<div className="flex gap-1">
<div
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
style={{ animationDelay: "0ms" }}
></div>
<div
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
style={{ animationDelay: "150ms" }}
></div>
<div
className="w-2 h-2 bg-gray-400 rounded-full animate-bounce"
style={{ animationDelay: "300ms" }}
></div>
</div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
{/* Quick Replies */}
{messages.length <= 1 && (
<div className="px-4 py-2 border-t bg-white">
<p className="text-xs text-gray-500 mb-2">
Perguntas frequentes:
</p>
<div className="flex flex-wrap gap-2">
{quickReplies.map((reply, index) => (
<button
key={index}
onClick={() => handleQuickReply(reply)}
className="text-xs bg-blue-50 hover:bg-blue-100 text-blue-700 px-3 py-1 rounded-full transition"
>
{reply}
</button>
))}
</div>
</div>
)}
{/* Input */}
<div className="p-4 border-t bg-white rounded-b-lg">
<div className="flex gap-2">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Digite sua mensagem..."
className="flex-1 border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
onClick={handleSend}
disabled={!inputValue.trim()}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed text-white rounded-lg p-2 transition"
aria-label="Enviar mensagem"
>
<Send className="w-5 h-5" />
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default Chatbot;

View File

@ -0,0 +1,151 @@
import React, { useState, useEffect } from "react";
import { Calendar, Clock, ArrowRight } from "lucide-react";
import { useNavigate } from "react-router-dom";
import { i18n } from "../i18n";
// Importar as imagens
import medico1 from "./images/medico1.jpg";
import medico2 from "./images/medico2.jpg";
import medico3 from "./images/medico3.jpg";
const images = [medico1, medico2, medico3];
export const HeroBanner: React.FC = () => {
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const [nextImageIndex, setNextImageIndex] = useState(1);
const [isTransitioning, setIsTransitioning] = useState(false);
const navigate = useNavigate();
useEffect(() => {
// Rotacionar imagens a cada 5 segundos
const interval = setInterval(() => {
setIsTransitioning(true);
// Após 2 segundos (duração da transição), atualizar os índices
setTimeout(() => {
setCurrentImageIndex((prev) => (prev + 1) % images.length);
setNextImageIndex((prev) => (prev + 1) % images.length);
setIsTransitioning(false);
}, 2000);
}, 5000);
return () => clearInterval(interval);
}, []);
const handleCTA = (action: string, destination: string) => {
console.log(`CTA clicked: ${action} -> ${destination}`);
navigate(destination);
};
return (
<div className="relative text-center py-8 md:py-12 lg:py-16 text-white rounded-xl shadow-lg overflow-hidden">
{/* Background Images com Fade Transition */}
<div className="absolute inset-0">
{/* Imagem Atual */}
<div
className={`absolute inset-0 bg-cover bg-center transition-opacity duration-2000 ${
isTransitioning ? "opacity-0" : "opacity-100"
}`}
style={{
backgroundImage: `url(${images[currentImageIndex]})`,
transitionDuration: "2000ms",
}}
/>
{/* Próxima Imagem (para transição suave) */}
<div
className={`absolute inset-0 bg-cover bg-center transition-opacity duration-2000 ${
isTransitioning ? "opacity-100" : "opacity-0"
}`}
style={{
backgroundImage: `url(${images[nextImageIndex]})`,
transitionDuration: "2000ms",
}}
/>
{/* Overlay Azul Translúcido */}
<div className="absolute inset-0 bg-blue-800/50" />
{/* Decorative Pattern */}
<div className="absolute inset-0 opacity-10">
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern
id="grid"
width="40"
height="40"
patternUnits="userSpaceOnUse"
>
<circle cx="20" cy="20" r="1" fill="white" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</div>
</div>
{/* Conteúdo */}
<div className="relative z-10 px-4 max-w-4xl mx-auto">
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-3 md:mb-4 drop-shadow-lg">
{i18n.t("home.hero.title")}
</h1>
<p className="text-base md:text-lg lg:text-xl opacity-95 mb-6 md:mb-8 max-w-2xl mx-auto drop-shadow-md">
{i18n.t("home.hero.subtitle")}
</p>
{/* CTAs */}
<div className="flex flex-col sm:flex-row gap-3 md:gap-4 justify-center items-center">
<button
onClick={() => handleCTA("Agendar consulta", "/paciente")}
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-white text-blue-700 rounded-lg font-semibold hover:bg-blue-50 hover:shadow-xl hover:scale-105 active:scale-95 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white/80 focus-visible:ring-offset-blue-600"
aria-label={i18n.t(
"home.actionCards.scheduleAppointment.ctaAriaLabel"
)}
>
<Calendar
className="w-5 h-5 mr-2 group-hover:scale-110 transition-transform"
aria-hidden="true"
/>
{i18n.t("home.hero.ctaPrimary")}
<ArrowRight
className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform"
aria-hidden="true"
/>
</button>
<button
onClick={() => handleCTA("Ver próximas consultas", "/paciente")}
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-blue-700 text-white rounded-lg font-semibold hover:bg-blue-800 hover:shadow-xl hover:scale-105 active:scale-95 border-2 border-white/20 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white/80 focus-visible:ring-offset-blue-600"
aria-label="Ver lista de próximas consultas"
>
<Clock className="w-5 h-5 mr-2" aria-hidden="true" />
{i18n.t("home.hero.ctaSecondary")}
</button>
</div>
</div>
{/* Indicadores de Imagem (opcionais - pequenos pontos na parte inferior) */}
<div className="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex gap-2 z-20">
{images.map((_, index) => (
<button
key={index}
onClick={() => {
setIsTransitioning(true);
setTimeout(() => {
setCurrentImageIndex(index);
setNextImageIndex((index + 1) % images.length);
setIsTransitioning(false);
}, 2000);
}}
className={`w-2 h-2 rounded-full transition-all duration-300 ${
index === currentImageIndex
? "bg-white w-6"
: "bg-white/50 hover:bg-white/75"
}`}
aria-label={`Ir para imagem ${index + 1}`}
/>
))}
</div>
</div>
);
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -24,6 +24,16 @@ export function SecretaryAppointmentList() {
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("Todos");
const [typeFilter, setTypeFilter] = useState("Todos");
const [showCreateModal, setShowCreateModal] = useState(false);
const [patients, setPatients] = useState<Patient[]>([]);
const [doctors, setDoctors] = useState<Doctor[]>([]);
const [formData, setFormData] = useState({
patient_id: "",
doctor_id: "",
scheduled_at: "",
appointment_type: "presencial",
notes: "",
});
const loadAppointments = async () => {
setLoading(true);
@ -68,8 +78,60 @@ export function SecretaryAppointmentList() {
useEffect(() => {
loadAppointments();
loadDoctorsAndPatients();
}, []);
const loadDoctorsAndPatients = async () => {
try {
const [patientsData, doctorsData] = await Promise.all([
patientService.list(),
doctorService.list(),
]);
setPatients(Array.isArray(patientsData) ? patientsData : []);
setDoctors(Array.isArray(doctorsData) ? doctorsData : []);
} catch (error) {
console.error("Erro ao carregar pacientes e médicos:", error);
}
};
const handleOpenCreateModal = () => {
setFormData({
patient_id: "",
doctor_id: "",
scheduled_at: "",
appointment_type: "presencial",
notes: "",
});
setShowCreateModal(true);
};
const handleCreateAppointment = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.patient_id || !formData.doctor_id || !formData.scheduled_at) {
toast.error("Preencha todos os campos obrigatórios");
return;
}
try {
await appointmentService.create({
patient_id: formData.patient_id,
doctor_id: formData.doctor_id,
scheduled_at: new Date(formData.scheduled_at).toISOString(),
appointment_type: formData.appointment_type as
| "presencial"
| "telemedicina",
});
toast.success("Consulta agendada com sucesso!");
setShowCreateModal(false);
loadAppointments();
} catch (error) {
console.error("Erro ao criar consulta:", error);
toast.error("Erro ao agendar consulta");
}
};
const handleSearch = () => {
loadAppointments();
};
@ -137,7 +199,10 @@ export function SecretaryAppointmentList() {
<h1 className="text-3xl font-bold text-gray-900">Consultas</h1>
<p className="text-gray-600 mt-1">Gerencie as consultas agendadas</p>
</div>
<button className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
<button
onClick={handleOpenCreateModal}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
<Plus className="h-4 w-4" />
Nova Consulta
</button>
@ -346,6 +411,116 @@ export function SecretaryAppointmentList() {
</tbody>
</table>
</div>
{/* Modal de Criar Consulta */}
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200">
<h2 className="text-2xl font-bold text-gray-900">
Nova Consulta
</h2>
</div>
<form onSubmit={handleCreateAppointment} className="p-6 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Paciente *
</label>
<select
value={formData.patient_id}
onChange={(e) =>
setFormData({ ...formData, patient_id: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
required
>
<option value="">Selecione um paciente</option>
{patients.map((patient) => (
<option key={patient.id} value={patient.id}>
{patient.full_name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Médico *
</label>
<select
value={formData.doctor_id}
onChange={(e) =>
setFormData({ ...formData, doctor_id: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
required
>
<option value="">Selecione um médico</option>
{doctors.map((doctor) => (
<option key={doctor.id} value={doctor.id}>
{doctor.full_name} - {doctor.specialty}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Data e Hora *
</label>
<input
type="datetime-local"
value={formData.scheduled_at}
onChange={(e) =>
setFormData({ ...formData, scheduled_at: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Tipo de Consulta *
</label>
<select
value={formData.appointment_type}
onChange={(e) =>
setFormData({
...formData,
appointment_type: e.target.value,
})
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
required
>
<option value="presencial">Presencial</option>
<option value="telemedicina">Telemedicina</option>
</select>
</div>
</div>
<div className="flex justify-end gap-3 pt-4">
<button
type="button"
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancelar
</button>
<button
type="submit"
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Agendar Consulta
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}

View File

@ -58,6 +58,7 @@ export function SecretaryDoctorSchedule() {
setLoading(true);
try {
// Load availabilities
const availData = await availabilityService.list({
doctor_id: selectedDoctorId,
});
@ -68,7 +69,6 @@ export function SecretaryDoctorSchedule() {
} catch (error) {
console.error("Erro ao carregar agenda:", error);
toast.error("Erro ao carregar agenda do médico");
setAvailabilities([]);
} finally {
setLoading(false);
}
@ -145,46 +145,14 @@ export function SecretaryDoctorSchedule() {
return;
}
if (!selectedDoctorId) {
toast.error("Selecione um médico");
return;
}
try {
console.log("📤 Criando disponibilidades para os dias:", selectedWeekdays);
// Cria uma disponibilidade para cada dia da semana selecionado
for (const weekday of selectedWeekdays) {
const availabilityData: any = {
doctor_id: selectedDoctorId,
weekday: weekday,
start_time: `${startTime}:00`,
end_time: `${endTime}:00`,
slot_minutes: duration,
appointment_type: "presencial",
active: true,
};
console.log("📤 Tentando criar disponibilidade:", availabilityData);
const result = await availabilityService.create(availabilityData);
console.log("✅ Disponibilidade criada:", result);
}
toast.success(`${selectedWeekdays.length} disponibilidade(s) criada(s) com sucesso!`);
// TODO: Implement availability creation
toast.success("Disponibilidade adicionada com sucesso");
setShowAvailabilityDialog(false);
// Limpa o formulário
setSelectedWeekdays([]);
setStartTime("08:00");
setEndTime("18:00");
setDuration(30);
// Recarrega as disponibilidades
await loadDoctorSchedule();
loadDoctorSchedule();
} catch (error) {
console.error("Erro ao adicionar disponibilidade:", error);
toast.error("Erro ao adicionar disponibilidade. Verifique as permissões no banco de dados.");
console.error("Erro ao adicionar disponibilidade:", error);
toast.error("Erro ao adicionar disponibilidade");
}
};
@ -204,14 +172,15 @@ export function SecretaryDoctorSchedule() {
toast.error("Erro ao adicionar exceção");
}
};
const weekdays = [
{ value: "segunda", label: "Segunda" },
{ value: "terca", label: "Terça" },
{ value: "quarta", label: "Quarta" },
{ value: "quinta", label: "Quinta" },
{ value: "sexta", label: "Sexta" },
{ value: "sabado", label: "Sábado" },
{ value: "domingo", label: "Domingo" },
{ value: "monday", label: "Segunda" },
{ value: "tuesday", label: "Terça" },
{ value: "wednesday", label: "Quarta" },
{ value: "thursday", label: "Quinta" },
{ value: "friday", label: "Sexta" },
{ value: "saturday", label: "Sábado" },
{ value: "sunday", label: "Domingo" },
];
return (
@ -343,16 +312,16 @@ export function SecretaryDoctorSchedule() {
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"
>
<div>
<p className="font-medium text-gray-900 capitalize">
{avail.weekday || "Não especificado"}
<p className="font-medium text-gray-900">
{avail.day_of_week}
</p>
<p className="text-sm text-gray-600">
{avail.start_time} - {avail.end_time} ({avail.slot_minutes || 30} min/consulta)
{avail.start_time} - {avail.end_time}
</p>
</div>
<div className="flex items-center gap-2">
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-700">
{avail.active !== false ? "Ativo" : "Inativo"}
Ativo
</span>
<button
title="Editar"

View File

@ -1,581 +0,0 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useState, useEffect } from "react";
import toast from "react-hot-toast";
import { Search, Plus, Eye, Calendar, Edit, Trash2, X, RefreshCw } from "lucide-react";
import { patientService, type Patient } from "../../services";
import PacienteForm, { type PacienteFormData } from "../pacientes/PacienteForm";
import { Avatar } from "../ui/Avatar";
import { validarCPF } from "../../utils/validators";
// Constantes
const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"];
const CONVENIOS = ["Particular", "Unimed", "Amil", "Bradesco Saúde", "SulAmérica", "Golden Cross"];
const COUNTRY_OPTIONS = [
{ value: "55", label: "+55 🇧🇷 Brasil" },
{ value: "1", label: "+1 🇺🇸 EUA/Canadá" },
];
// Função para buscar endereço via CEP
const buscarEnderecoViaCEP = async (cep: string) => {
try {
const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
const data = await response.json();
if (data.erro) return null;
return {
rua: data.logradouro,
bairro: data.bairro,
cidade: data.localidade,
estado: data.uf,
cep: data.cep,
};
} catch {
return null;
}
};
export function SecretaryPatientList() {
const [patients, setPatients] = useState<Patient[]>([]);
const [loading, setLoading] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const [insuranceFilter, setInsuranceFilter] = useState("Todos");
const [showBirthdays, setShowBirthdays] = useState(false);
const [showVIP, setShowVIP] = useState(false);
// Modal states
const [showModal, setShowModal] = useState(false);
const [modalMode, setModalMode] = useState<"create" | "edit">("create");
const [formData, setFormData] = useState<PacienteFormData>({
nome: "",
social_name: "",
cpf: "",
sexo: "",
dataNascimento: "",
email: "",
codigoPais: "55",
ddd: "",
numeroTelefone: "",
tipo_sanguineo: "",
altura: "",
peso: "",
convenio: "Particular",
numeroCarteirinha: "",
observacoes: "",
endereco: {
cep: "",
rua: "",
numero: "",
bairro: "",
cidade: "",
estado: "",
},
});
const [cpfError, setCpfError] = useState<string | null>(null);
const [cpfValidationMessage, setCpfValidationMessage] = useState<
string | null
>(null);
const loadPatients = async () => {
setLoading(true);
try {
const data = await patientService.list();
setPatients(Array.isArray(data) ? data : []);
} catch (error: any) {
console.error("Erro ao carregar pacientes:", error);
toast.error("Erro ao carregar lista de pacientes");
setPatients([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadPatients();
}, []);
const handleSearch = () => {
loadPatients();
};
const handleClear = () => {
setSearchTerm("");
setInsuranceFilter("Todos");
setShowBirthdays(false);
setShowVIP(false);
loadPatients();
};
const handleNewPatient = () => {
setModalMode("create");
setFormData({
nome: "",
social_name: "",
cpf: "",
sexo: "",
dataNascimento: "",
email: "",
codigoPais: "55",
ddd: "",
numeroTelefone: "",
tipo_sanguineo: "",
altura: "",
peso: "",
convenio: "Particular",
numeroCarteirinha: "",
observacoes: "",
endereco: {
cep: "",
rua: "",
numero: "",
bairro: "",
cidade: "",
estado: "",
},
});
setCpfError(null);
setCpfValidationMessage(null);
setShowModal(true);
};
const handleEditPatient = (patient: Patient) => {
setModalMode("edit");
setFormData({
id: patient.id,
nome: patient.full_name || "",
social_name: patient.social_name || "",
cpf: patient.cpf || "",
sexo: patient.sex || "",
dataNascimento: patient.birth_date || "",
email: patient.email || "",
codigoPais: "55",
ddd: "",
numeroTelefone: patient.phone_mobile || "",
tipo_sanguineo: patient.blood_type || "",
altura: patient.height_m?.toString() || "",
peso: patient.weight_kg?.toString() || "",
convenio: "Particular",
numeroCarteirinha: "",
observacoes: "",
endereco: {
cep: patient.cep || "",
rua: patient.street || "",
numero: patient.number || "",
complemento: patient.complement || "",
bairro: patient.neighborhood || "",
cidade: patient.city || "",
estado: patient.state || "",
},
});
setCpfError(null);
setCpfValidationMessage(null);
setShowModal(true);
};
const handleFormChange = (patch: Partial<PacienteFormData>) => {
setFormData((prev) => ({ ...prev, ...patch }));
};
const handleCpfChange = (value: string) => {
setFormData((prev) => ({ ...prev, cpf: value }));
setCpfError(null);
setCpfValidationMessage(null);
};
const handleCepLookup = async (cep: string) => {
const endereco = await buscarEnderecoViaCEP(cep);
if (endereco) {
setFormData((prev) => ({
...prev,
endereco: {
...prev.endereco,
...endereco,
},
}));
toast.success("Endereço encontrado!");
} else {
toast.error("CEP não encontrado");
}
};
const handleFormSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
try {
if (modalMode === "edit" && formData.id) {
// Para edição, usa o endpoint antigo (PATCH /patients/:id)
const patientData = {
full_name: formData.nome,
social_name: formData.social_name || null,
cpf: formData.cpf,
sex: formData.sexo || null,
birth_date: formData.dataNascimento || null,
email: formData.email,
phone_mobile: formData.numeroTelefone,
blood_type: formData.tipo_sanguineo || null,
height_m: formData.altura ? parseFloat(formData.altura) : null,
weight_kg: formData.peso ? parseFloat(formData.peso) : null,
cep: formData.endereco.cep || null,
street: formData.endereco.rua || null,
number: formData.endereco.numero || null,
complement: formData.endereco.complemento || null,
neighborhood: formData.endereco.bairro || null,
city: formData.endereco.cidade || null,
state: formData.endereco.estado || null,
};
await patientService.update(formData.id, patientData);
toast.success("Paciente atualizado com sucesso!");
} else {
// Para criação, apenas cria o registro na tabela patients
// O usuário de autenticação pode ser criado depois quando necessário
// Validação dos campos obrigatórios no frontend
if (!formData.email || !formData.nome || !formData.cpf) {
toast.error("Por favor, preencha os campos obrigatórios: Email, Nome e CPF");
return;
}
// Remove formatação do CPF (deixa apenas números)
const cpfLimpo = formData.cpf.replace(/\D/g, "");
if (cpfLimpo.length !== 11) {
toast.error("CPF deve ter 11 dígitos");
return;
}
// Valida CPF
if (!validarCPF(cpfLimpo)) {
toast.error("CPF inválido. Verifique os dígitos verificadores.");
return;
}
// Monta o telefone completo
const ddd = (formData.ddd || "").replace(/\D/g, "");
const numero = (formData.numeroTelefone || "").replace(/\D/g, "");
// Validação do telefone
if (!ddd || !numero) {
toast.error("Por favor, preencha o DDD e o número do telefone");
return;
}
if (ddd.length !== 2) {
toast.error("DDD deve ter 2 dígitos");
return;
}
if (numero.length < 8 || numero.length > 9) {
toast.error("Número do telefone deve ter 8 ou 9 dígitos");
return;
}
// Monta telefone no formato: (11) 99999-9999
const telefoneLimpo = `(${ddd}) ${numero.length === 9 ? numero.substring(0, 5) + '-' + numero.substring(5) : numero.substring(0, 4) + '-' + numero.substring(4)}`;
// Cria apenas o registro na tabela patients
const patientData = {
full_name: formData.nome.trim(),
cpf: cpfLimpo,
email: formData.email.trim(),
phone_mobile: telefoneLimpo,
birth_date: formData.dataNascimento || null,
sex: formData.sexo || null,
blood_type: formData.tipo_sanguineo || null,
// Converte altura de cm para metros (ex: 180 cm = 1.80 m)
height_m: formData.altura && !isNaN(parseFloat(formData.altura))
? parseFloat(formData.altura) / 100
: null,
weight_kg: formData.peso && !isNaN(parseFloat(formData.peso))
? parseFloat(formData.peso)
: null,
cep: formData.endereco.cep || null,
street: formData.endereco.rua || null,
number: formData.endereco.numero || null,
complement: formData.endereco.complemento || null,
neighborhood: formData.endereco.bairro || null,
city: formData.endereco.cidade || null,
state: formData.endereco.estado || null,
};
const patientResult = await patientService.create(patientData);
toast.success("Paciente cadastrado com sucesso!");
}
setShowModal(false);
// Aguarda um pouco antes de recarregar para o banco propagar
await new Promise(resolve => setTimeout(resolve, 1000));
await loadPatients();
} catch (error: any) {
console.error("Erro ao salvar paciente:", error);
let errorMessage = "Erro ao salvar paciente. Verifique os dados e tente novamente.";
if (error?.response?.data) {
const data = error.response.data;
errorMessage = data.error || data.message || data.details || JSON.stringify(data);
} else if (error?.message) {
errorMessage = error.message;
}
toast.error(errorMessage);
} finally {
setLoading(false);
}
};
};
const handleCancelForm = () => {
setShowModal(false);
};
const getPatientColor = (
index: number
): "blue" | "green" | "purple" | "orange" | "pink" | "teal" => {
const colors: Array<
"blue" | "green" | "purple" | "orange" | "pink" | "teal"
> = ["blue", "green", "purple", "orange", "pink", "teal"];
return colors[index % colors.length];
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Pacientes</h1>
<p className="text-gray-600 mt-1">
Gerencie os pacientes cadastrados
</p>
</div>
<div className="flex gap-2">
<button
onClick={loadPatients}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Recarregar
</button>
<button
onClick={handleNewPatient}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
<Plus className="h-4 w-4" />
Novo Paciente
</button>
</div>
</div>
{/* Search and Filters */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
<div className="flex gap-3">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Buscar pacientes por nome ou email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
/>
</div>
<button
onClick={handleSearch}
className="px-6 py-2.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Buscar
</button>
<button
onClick={handleClear}
className="px-6 py-2.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Limpar
</button>
</div>
<div className="flex items-center gap-6">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={showBirthdays}
onChange={(e) => setShowBirthdays(e.target.checked)}
className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
/>
<span className="text-sm text-gray-700">
Aniversariantes do mês
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={showVIP}
onChange={(e) => setShowVIP(e.target.checked)}
className="h-4 w-4 text-green-600 border-gray-300 rounded focus:ring-green-500"
/>
<span className="text-sm text-gray-700">Somente VIP</span>
</label>
<div className="flex items-center gap-2 ml-auto">
<span className="text-sm text-gray-600">Convênio:</span>
<select
value={insuranceFilter}
onChange={(e) => setInsuranceFilter(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-transparent"
>
<option>Todos</option>
<option>Particular</option>
<option>Unimed</option>
<option>Amil</option>
<option>Bradesco Saúde</option>
</select>
</div>
</div>
</div>
{/* Table */}
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">
Paciente
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">
Próximo Atendimento
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">
Convênio
</th>
<th className="px-6 py-4 text-left text-sm font-semibold text-gray-700 uppercase tracking-wider">
Ações
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{loading ? (
<tr>
<td
colSpan={4}
className="px-6 py-12 text-center text-gray-500"
>
Carregando pacientes...
</td>
</tr>
) : patients.length === 0 ? (
<tr>
<td
colSpan={4}
className="px-6 py-12 text-center text-gray-500"
>
Nenhum paciente encontrado
</td>
</tr>
) : (
patients.map((patient, index) => (
<tr
key={patient.id}
className="hover:bg-gray-50 transition-colors"
>
<td className="px-6 py-4">
<div className="flex items-center gap-3">
<Avatar
src={patient}
name={patient.full_name || ""}
size="md"
color={getPatientColor(index)}
/>
<div>
<p className="text-sm font-medium text-gray-900">
{patient.full_name}
</p>
<p className="text-sm text-gray-500">{patient.email}</p>
<p className="text-sm text-gray-500">
{patient.phone_mobile}
</p>
</div>
</div>
</td>
<td className="px-6 py-4 text-sm text-gray-700">
{/* TODO: Buscar próximo agendamento */}
</td>
<td className="px-6 py-4 text-sm text-gray-700">
Particular
</td>
<td className="px-6 py-4">
<div className="flex items-center gap-2">
<button
title="Visualizar"
className="p-2 text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
>
<Eye className="h-4 w-4" />
</button>
<button
title="Agendar consulta"
className="p-2 text-green-600 hover:bg-green-50 rounded-lg transition-colors"
>
<Calendar className="h-4 w-4" />
</button>
<button
onClick={() => handleEditPatient(patient)}
title="Editar"
className="p-2 text-orange-600 hover:bg-orange-50 rounded-lg transition-colors"
>
<Edit className="h-4 w-4" />
</button>
<button
title="Deletar"
className="p-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
<Trash2 className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Modal de Formulário */}
{showModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl max-w-4xl w-full max-h-[90vh] overflow-hidden flex flex-col">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200">
<h2 className="text-xl font-semibold text-gray-900">
{modalMode === "create" ? "Novo Paciente" : "Editar Paciente"}
</h2>
<button
onClick={handleCancelForm}
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
>
<X className="h-5 w-5" />
</button>
</div>
{/* Form Content */}
<div className="flex-1 overflow-y-auto p-6">
<PacienteForm
mode={modalMode}
loading={loading}
data={formData}
bloodTypes={BLOOD_TYPES}
convenios={CONVENIOS}
countryOptions={COUNTRY_OPTIONS}
cpfError={cpfError}
cpfValidationMessage={cpfValidationMessage}
onChange={handleFormChange}
onCpfChange={handleCpfChange}
onCepLookup={handleCepLookup}
onCancel={handleCancelForm}
onSubmit={handleFormSubmit}
/>
</div>
</div>
</div>
)}
</div>
);
}

View File

@ -1,10 +1,9 @@
import { useState, useEffect } from "react";
import toast from "react-hot-toast";
import { Search, Plus, Eye, Calendar, Edit, Trash2, X, RefreshCw } from "lucide-react";
import { patientService, type Patient } from "../../services";
import { Search, Plus, Eye, Calendar, Edit, Trash2, X } from "lucide-react";
import { patientService, userService, type Patient } from "../../services";
import PacienteForm, { type PacienteFormData } from "../pacientes/PacienteForm";
import { Avatar } from "../ui/Avatar";
import { validarCPF } from "../../utils/validators";
const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"];
@ -84,51 +83,14 @@ export function SecretaryPatientList() {
const loadPatients = async () => {
setLoading(true);
try {
console.log("🔄 Carregando lista de pacientes...");
const data = await patientService.list();
console.log("✅ Pacientes carregados:", {
total: Array.isArray(data) ? data.length : 0,
isArray: Array.isArray(data),
dataType: typeof data,
primeiros3: Array.isArray(data) ? data.slice(0, 3) : null,
todosOsDados: data
});
// Verifica se há pacientes e se eles têm os campos necessários
if (Array.isArray(data) && data.length > 0) {
console.log("📋 Exemplo do primeiro paciente:", data[0]);
console.log("📋 Campos disponíveis:", Object.keys(data[0] || {}));
// Busca específica pelo paciente "teste squad 18"
const testeSquad = data.find(p =>
p.full_name?.toLowerCase().includes("teste squad")
);
if (testeSquad) {
console.log("✅ PACIENTE 'teste squad 18' ENCONTRADO:", testeSquad);
} else {
console.warn("❌ PACIENTE 'teste squad 18' NÃO ENCONTRADO");
console.log("📋 Lista de nomes dos pacientes:", data.map(p => p.full_name));
}
}
console.log("💾 Atualizando estado com", data.length, "pacientes");
console.log("✅ Pacientes carregados:", data);
setPatients(Array.isArray(data) ? data : []);
// Verifica o estado logo após setar
console.log("💾 Estado patients após setPatients:", {
length: Array.isArray(data) ? data.length : 0,
isArray: Array.isArray(data)
});
if (Array.isArray(data) && data.length === 0) {
console.warn("⚠️ Nenhum paciente encontrado na API");
}
} catch (error: any) {
console.error("❌ Erro ao carregar pacientes:", {
message: error?.message,
response: error?.response?.data,
status: error?.response?.status
});
} catch (error) {
console.error("❌ Erro ao carregar pacientes:", error);
toast.error("Erro ao carregar pacientes");
setPatients([]);
} finally {
@ -273,120 +235,34 @@ export function SecretaryPatientList() {
await patientService.update(formData.id, patientData);
toast.success("Paciente atualizado com sucesso!");
} else {
// Para criação, apenas cria o registro na tabela patients
// O usuário de autenticação pode ser criado depois quando necessário
// Validação dos campos obrigatórios no frontend
if (!formData.email || !formData.nome || !formData.cpf) {
toast.error("Por favor, preencha os campos obrigatórios: Email, Nome e CPF");
return;
}
// Remove formatação do CPF (deixa apenas números)
const cpfLimpo = formData.cpf.replace(/\D/g, "");
if (cpfLimpo.length !== 11) {
toast.error("CPF deve ter 11 dígitos");
return;
}
// Valida CPF
if (!validarCPF(cpfLimpo)) {
toast.error("CPF inválido. Verifique os dígitos verificadores.");
return;
}
// Monta o telefone completo
const ddd = (formData.ddd || "").replace(/\D/g, "");
const numero = (formData.numeroTelefone || "").replace(/\D/g, "");
// Validação do telefone
if (!ddd || !numero) {
toast.error("Por favor, preencha o DDD e o número do telefone");
return;
}
if (ddd.length !== 2) {
toast.error("DDD deve ter 2 dígitos");
return;
}
if (numero.length < 8 || numero.length > 9) {
toast.error("Número do telefone deve ter 8 ou 9 dígitos");
return;
}
// Monta telefone no formato: (11) 99999-9999
const telefoneLimpo = `(${ddd}) ${numero.length === 9 ? numero.substring(0, 5) + '-' + numero.substring(5) : numero.substring(0, 4) + '-' + numero.substring(4)}`;
// Cria apenas o registro na tabela patients
const patientData = {
full_name: formData.nome.trim(),
cpf: cpfLimpo,
email: formData.email.trim(),
phone_mobile: telefoneLimpo,
birth_date: formData.dataNascimento || null,
sex: formData.sexo || null,
blood_type: formData.tipo_sanguineo || null,
// Converte altura de cm para metros (ex: 180 cm = 1.80 m)
height_m: formData.altura && !isNaN(parseFloat(formData.altura))
? parseFloat(formData.altura) / 100
: null,
weight_kg: formData.peso && !isNaN(parseFloat(formData.peso))
? parseFloat(formData.peso)
: null,
cep: formData.endereco.cep || null,
street: formData.endereco.rua || null,
number: formData.endereco.numero || null,
complement: formData.endereco.complemento || null,
neighborhood: formData.endereco.bairro || null,
city: formData.endereco.cidade || null,
state: formData.endereco.estado || null,
// Para criação, usa o novo endpoint create-patient com validações completas
const createData = {
email: formData.email,
full_name: formData.nome,
cpf: formData.cpf,
phone_mobile: formData.numeroTelefone,
birth_date: formData.dataNascimento || undefined,
address: formData.endereco.rua
? `${formData.endereco.rua}${
formData.endereco.numero ? ", " + formData.endereco.numero : ""
}${
formData.endereco.bairro ? " - " + formData.endereco.bairro : ""
}${
formData.endereco.cidade ? " - " + formData.endereco.cidade : ""
}${
formData.endereco.estado ? "/" + formData.endereco.estado : ""
}`
: undefined,
};
console.log("📤 Criando registro de paciente:", patientData);
console.log("📤 Tipos dos campos:", {
height_m: typeof patientData.height_m,
weight_kg: typeof patientData.weight_kg,
height_value: patientData.height_m,
weight_value: patientData.weight_kg,
});
const patientResult = await patientService.create(patientData);
console.log("✅ Paciente criado na tabela patients:", patientResult);
await userService.createPatient(createData);
toast.success("Paciente cadastrado com sucesso!");
}
setShowModal(false);
// Aguarda um pouco antes de recarregar para o banco propagar
console.log("⏳ Aguardando 1 segundo antes de recarregar a lista...");
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("🔄 Recarregando lista de pacientes...");
await loadPatients();
} catch (error: any) {
console.error("❌ Erro ao salvar paciente:", error);
console.error("❌ Detalhes do erro:", {
message: error?.message,
response: error?.response,
responseData: error?.response?.data,
status: error?.response?.status,
statusText: error?.response?.statusText,
});
// Exibe mensagem de erro mais específica
let errorMessage = "Erro ao salvar paciente";
if (error?.response?.data) {
const data = error.response.data;
errorMessage = data.error || data.message || data.details || JSON.stringify(data);
} else if (error?.message) {
errorMessage = error.message;
}
console.error("❌ Mensagem final de erro:", errorMessage);
toast.error(errorMessage);
loadPatients();
} catch (error) {
console.error("Erro ao salvar paciente:", error);
toast.error("Erro ao salvar paciente");
} finally {
setLoading(false);
}
@ -405,14 +281,6 @@ export function SecretaryPatientList() {
return colors[index % colors.length];
};
// Log para debug do estado atual
console.log("🎨 Renderizando SecretaryPatientList:", {
totalPacientes: patients.length,
loading: loading,
temPacientes: patients.length > 0,
primeiros2: patients.slice(0, 2)
});
return (
<div className="space-y-6">
{/* Header */}
@ -423,23 +291,13 @@ export function SecretaryPatientList() {
Gerencie os pacientes cadastrados
</p>
</div>
<div className="flex gap-2">
<button
onClick={loadPatients}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
Recarregar
</button>
<button
onClick={handleNewPatient}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
<Plus className="h-4 w-4" />
Novo Paciente
</button>
</div>
<button
onClick={handleNewPatient}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
<Plus className="h-4 w-4" />
Novo Paciente
</button>
</div>
{/* Search and Filters */}
@ -607,8 +465,7 @@ export function SecretaryPatientList() {
</td>
</tr>
))
)
}
)}
</tbody>
</table>
</div>

View File

@ -1,7 +1,12 @@
import { useState, useEffect } from "react";
import toast from "react-hot-toast";
import { Search, FileText, Download } from "lucide-react";
import { reportService, type Report } from "../../services";
import { Search, FileText, Download, Plus } from "lucide-react";
import {
reportService,
type Report,
patientService,
type Patient,
} from "../../services";
export function SecretaryReportList() {
const [reports, setReports] = useState<Report[]>([]);
@ -9,11 +14,64 @@ export function SecretaryReportList() {
const [searchTerm, setSearchTerm] = useState("");
const [typeFilter, setTypeFilter] = useState("Todos");
const [periodFilter, setPeriodFilter] = useState("Todos");
const [showCreateModal, setShowCreateModal] = useState(false);
const [patients, setPatients] = useState<Patient[]>([]);
const [formData, setFormData] = useState({
patient_id: "",
exam: "",
diagnosis: "",
conclusion: "",
});
useEffect(() => {
loadReports();
loadPatients();
}, []);
const loadPatients = async () => {
try {
const data = await patientService.list();
setPatients(Array.isArray(data) ? data : []);
} catch (error) {
console.error("Erro ao carregar pacientes:", error);
}
};
const handleOpenCreateModal = () => {
setFormData({
patient_id: "",
exam: "",
diagnosis: "",
conclusion: "",
});
setShowCreateModal(true);
};
const handleCreateReport = async (e: React.FormEvent) => {
e.preventDefault();
if (!formData.patient_id) {
toast.error("Selecione um paciente");
return;
}
try {
await reportService.create({
patient_id: formData.patient_id,
exam: formData.exam,
diagnosis: formData.diagnosis,
conclusion: formData.conclusion,
});
toast.success("Relatório criado com sucesso!");
setShowCreateModal(false);
loadReports();
} catch (error) {
console.error("Erro ao criar relatório:", error);
toast.error("Erro ao criar relatório");
}
};
const loadReports = async () => {
setLoading(true);
try {
@ -68,9 +126,12 @@ export function SecretaryReportList() {
Visualize e baixe relatórios do sistema
</p>
</div>
<button className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors">
<FileText className="h-4 w-4" />
Gerar Relatório
<button
onClick={handleOpenCreateModal}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
<Plus className="h-4 w-4" />
Novo Relatório
</button>
</div>
@ -242,6 +303,103 @@ export function SecretaryReportList() {
</tbody>
</table>
</div>
{/* Modal de Criar Relatório */}
{showCreateModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-gray-200">
<h2 className="text-2xl font-bold text-gray-900">
Novo Relatório
</h2>
</div>
<form onSubmit={handleCreateReport} className="p-6 space-y-4">
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Paciente *
</label>
<select
value={formData.patient_id}
onChange={(e) =>
setFormData({ ...formData, patient_id: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
required
>
<option value="">Selecione um paciente</option>
{patients.map((patient) => (
<option key={patient.id} value={patient.id}>
{patient.full_name}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Exame
</label>
<input
type="text"
value={formData.exam}
onChange={(e) =>
setFormData({ ...formData, exam: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500"
placeholder="Nome do exame realizado"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Diagnóstico
</label>
<textarea
value={formData.diagnosis}
onChange={(e) =>
setFormData({ ...formData, diagnosis: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 h-24"
placeholder="Diagnóstico do paciente"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Conclusão
</label>
<textarea
value={formData.conclusion}
onChange={(e) =>
setFormData({ ...formData, conclusion: e.target.value })
}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 h-24"
placeholder="Conclusão e recomendações"
/>
</div>
</div>
<div className="flex justify-end gap-3 pt-4">
<button
type="button"
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
>
Cancelar
</button>
<button
type="submit"
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
>
Criar Relatório
</button>
</div>
</form>
</div>
</div>
)}
</div>
);
}

View File

@ -14,6 +14,7 @@ import {
Headphones,
ArrowLeft,
} from "lucide-react";
import Chatbot from "../components/Chatbot";
interface FAQ {
question: string;
@ -404,6 +405,9 @@ const CentralAjuda: React.FC = () => {
</div>
</div>
</div>
{/* Chatbot Widget */}
<Chatbot />
</div>
);
};

View File

@ -14,6 +14,7 @@ import {
Headphones,
ArrowLeft,
} from "lucide-react";
import Chatbot from "../components/Chatbot";
interface FAQ {
question: string;
@ -408,6 +409,9 @@ const CentralAjudaMedico: React.FC = () => {
</div>
</div>
</div>
{/* Chatbot Widget */}
<Chatbot />
</div>
);
};

View File

@ -3,6 +3,7 @@ import { Calendar, Users, UserCheck, Clock, ArrowRight } from "lucide-react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { patientService, doctorService, appointmentService } from "../services";
import { MetricCard } from "../components/MetricCard";
import { HeroBanner } from "../components/HeroBanner";
import { i18n } from "../i18n";
import { useAuth } from "../hooks/useAuth";
@ -96,64 +97,8 @@ const Home: React.FC = () => {
return (
<div className="space-y-8" id="main-content">
{/* Hero Section */}
<div className="relative text-center py-8 md:py-12 lg:py-16 bg-gradient-to-r from-blue-800 via-blue-600 to-blue-500 text-white rounded-xl shadow-lg overflow-hidden">
{/* Decorative Pattern */}
<div className="absolute inset-0 opacity-10">
<svg width="100%" height="100%" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern
id="grid"
width="40"
height="40"
patternUnits="userSpaceOnUse"
>
<circle cx="20" cy="20" r="1" fill="white" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
</div>
<div className="relative z-10 px-4 max-w-4xl mx-auto">
<h1 className="text-3xl md:text-4xl lg:text-5xl font-bold mb-3 md:mb-4">
{i18n.t("home.hero.title")}
</h1>
<p className="text-base md:text-lg lg:text-xl opacity-95 mb-6 md:mb-8 max-w-2xl mx-auto">
{i18n.t("home.hero.subtitle")}
</p>
{/* CTAs */}
<div className="flex flex-col sm:flex-row gap-3 md:gap-4 justify-center items-center">
<button
onClick={() => handleCTA("Agendar consulta", "/paciente")}
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-white text-blue-700 rounded-lg font-semibold hover:bg-blue-50 hover:shadow-xl hover:scale-105 active:scale-95 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white/80 focus-visible:ring-offset-blue-600"
aria-label={i18n.t(
"home.actionCards.scheduleAppointment.ctaAriaLabel"
)}
>
<Calendar
className="w-5 h-5 mr-2 group-hover:scale-110 transition-transform"
aria-hidden="true"
/>
{i18n.t("home.hero.ctaPrimary")}
<ArrowRight
className="w-5 h-5 ml-2 group-hover:translate-x-1 transition-transform"
aria-hidden="true"
/>
</button>
<button
onClick={() => handleCTA("Ver próximas consultas", "/consultas")}
className="group w-full sm:w-auto inline-flex items-center justify-center px-6 md:px-8 py-3 md:py-4 bg-blue-700 text-white rounded-lg font-semibold hover:bg-blue-800 hover:shadow-xl hover:scale-105 active:scale-95 border-2 border-white/20 transition-all duration-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-white/80 focus-visible:ring-offset-blue-600"
aria-label="Ver lista de próximas consultas"
>
<Clock className="w-5 h-5 mr-2" aria-hidden="true" />
{i18n.t("home.hero.ctaSecondary")}
</button>
</div>
</div>
</div>
{/* Hero Section com Background Rotativo */}
<HeroBanner />
{/* Métricas */}
<div

View File

@ -3,6 +3,16 @@ import AvatarInitials from "../components/AvatarInitials";
import { Stethoscope, Mail, Phone, AlertTriangle } from "lucide-react";
import { doctorService } from "../services";
interface MedicoDetalhado {
id: string;
nome: string;
especialidade: string;
crm: string;
email: string;
telefone?: string;
avatar_url?: string;
}
const ListaMedicos: React.FC = () => {
const [medicos, setMedicos] = useState<MedicoDetalhado[]>([]);
const [loading, setLoading] = useState<boolean>(true);
@ -14,21 +24,25 @@ const ListaMedicos: React.FC = () => {
setLoading(true);
setError(null);
try {
const resp = await doctorService.listarMedicos({ status: "ativo" });
if (!resp.success) {
if (!cancelled) {
setError(resp.error || "Falha ao carregar médicos");
setMedicos([]);
}
return;
}
const list = resp.data?.data || [];
const list = await doctorService.list({ active: true });
if (!list.length) {
console.warn(
'[ListaMedicos] Nenhum médico retornado. Verifique se a tabela "doctors" possui registros e se as variáveis VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY apontam para produção.'
);
}
if (!cancelled) setMedicos(list);
if (!cancelled) {
// Mapear para o formato esperado pelo componente
const medicosFormatados = list.map((doctor) => ({
id: doctor.id,
nome: doctor.full_name,
especialidade: doctor.specialty || "Não informado",
crm: doctor.crm,
email: doctor.email,
telefone: doctor.phone_mobile || "",
avatar_url: undefined,
}));
setMedicos(medicosFormatados);
}
} catch (e) {
console.error("Erro inesperado ao listar médicos", e);
if (!cancelled) {

View File

@ -3,7 +3,7 @@ import { Mail, Lock, Stethoscope } from "lucide-react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
import { authService } from "../services";
import { authService, userService } from "../services";
const LoginMedico: React.FC = () => {
const [formData, setFormData] = useState({
@ -22,21 +22,61 @@ const LoginMedico: React.FC = () => {
try {
console.log("[LoginMedico] Fazendo login com email:", formData.email);
// Fazer login via API Supabase
const loginResponse = await authService.login({
email: formData.email,
password: formData.senha,
});
console.log("[LoginMedico] Login bem-sucedido!", loginResponse);
// Buscar informações completas do usuário (profile + roles)
const userInfo = await userService.getUserInfo();
console.log("[LoginMedico] UserInfo obtido:", userInfo);
const userName =
userInfo.profile?.full_name ||
loginResponse.user.email?.split("@")[0] ||
"Médico";
const roles = userInfo.roles || [];
// Validar se tem permissão (admin, gestor ou medico)
const isAdmin = roles.includes("admin");
const isGestor = roles.includes("gestor");
const isMedico = roles.includes("medico");
if (!isAdmin && !isGestor && !isMedico) {
toast.error("Você não tem permissão para acessar esta área");
await authService.logout();
setLoading(false);
return;
}
// Fazer login no contexto
const ok = await loginComEmailSenha(formData.email, formData.senha);
if (ok) {
console.log(
"[LoginMedico] Login bem-sucedido! Navegando para /painel-medico"
);
toast.success("Login realizado com sucesso!");
toast.success(`Bem-vindo, ${userName}!`);
navigate("/painel-medico");
} else {
console.error("[LoginMedico] loginComEmailSenha retornou false");
toast.error("Credenciais inválidas ou usuário sem permissão");
toast.error("Erro ao processar login");
}
} catch (error) {
} catch (error: unknown) {
console.error("[LoginMedico] Erro no login:", error);
toast.error("Erro ao fazer login. Verifique suas credenciais.");
const err = error as {
response?: { data?: { error_description?: string; message?: string } };
message?: string;
};
const errorMessage =
err?.response?.data?.error_description ||
err?.response?.data?.message ||
err?.message ||
"Erro ao fazer login. Verifique suas credenciais.";
toast.error(errorMessage);
} finally {
setLoading(false);
}
@ -117,16 +157,15 @@ const LoginMedico: React.FC = () => {
return;
}
try {
await authService.sendMagicLink(
formData.email,
"https://mediconnectbrasil.netlify.app/medico/painel"
await authService.requestPasswordReset(formData.email);
toast.success(
"Email de recuperação enviado! Verifique sua caixa de entrada."
);
toast.success("Link de acesso enviado para seu email!");
} catch {
toast.error("Erro ao enviar link");
toast.error("Erro ao enviar email de recuperação");
}
}}
className="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 hover:underline transition-colors"
className="text-sm text-indigo-600 dark:text-indigo-400 hover:underline transition-colors"
>
Esqueceu a senha?
</button>

View File

@ -3,7 +3,7 @@ import { User, Mail, Lock } from "lucide-react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
import { authService, patientService } from "../services";
import { authService, patientService, userService } from "../services";
const LoginPaciente: React.FC = () => {
const [formData, setFormData] = useState({
@ -32,47 +32,65 @@ const LoginPaciente: React.FC = () => {
console.log("[LoginPaciente] Fazendo login com email:", formData.email);
// Fazer login via API Supabase
await authService.login({
const loginResponse = await authService.login({
email: formData.email,
password: formData.senha,
});
console.log("[LoginPaciente] Login bem-sucedido!");
console.log("[LoginPaciente] Login bem-sucedido!", loginResponse);
// Buscar dados do paciente da API
const pacientes = await patientService.list();
const paciente = pacientes.find((p: any) => p.email === formData.email);
// Buscar informações completas do usuário (profile + roles)
let userName = loginResponse.user.email?.split("@")[0] || "Paciente";
try {
const userInfo = await userService.getUserInfo();
console.log("[LoginPaciente] UserInfo obtido:", userInfo);
// Pegar o nome do profile
if (userInfo.profile?.full_name) {
userName = userInfo.profile.full_name;
}
} catch {
console.warn(
"[LoginPaciente] Não foi possível obter user-info, usando dados básicos"
);
}
// Tentar buscar dados do paciente da tabela patients
const pacientes = await patientService.list({ email: formData.email });
const paciente = pacientes && pacientes.length > 0 ? pacientes[0] : null;
console.log("[LoginPaciente] Paciente encontrado:", paciente);
if (paciente) {
console.log("[LoginPaciente] Paciente encontrado:", {
id: paciente.id,
nome: paciente.full_name,
email: paciente.email,
});
const ok = await loginPaciente({
id: paciente.id,
nome: paciente.full_name,
email: paciente.email,
});
// Usar nome do paciente se disponível, senão usar do profile
const finalName = paciente?.full_name || userName;
if (ok) {
console.log("[LoginPaciente] Navegando para /acompanhamento");
navigate("/acompanhamento");
} else {
console.error("[LoginPaciente] loginPaciente retornou false");
toast.error("Erro ao processar login");
}
const ok = await loginPaciente({
id: paciente?.id || loginResponse.user.id,
nome: finalName,
email: loginResponse.user.email || formData.email,
});
if (ok) {
console.log("[LoginPaciente] Navegando para /acompanhamento");
toast.success(`Bem-vindo, ${finalName}!`);
navigate("/acompanhamento");
} else {
console.log("[LoginPaciente] Paciente não encontrado na lista");
toast.error(
"Dados do paciente não encontrados. Entre em contato com o suporte."
);
console.error("[LoginPaciente] loginPaciente retornou false");
toast.error("Erro ao processar login");
}
} catch (error) {
} catch (error: unknown) {
console.error("[LoginPaciente] Erro no login:", error);
toast.error("Erro ao fazer login. Tente novamente.");
const err = error as {
response?: { data?: { error_description?: string; message?: string } };
message?: string;
};
const errorMessage =
err?.response?.data?.error_description ||
err?.response?.data?.message ||
err?.message ||
"Erro ao fazer login. Verifique suas credenciais.";
toast.error(errorMessage);
} finally {
setLoading(false);
}
@ -141,16 +159,20 @@ const LoginPaciente: React.FC = () => {
});
setShowCadastro(false);
} catch (error: any) {
} catch (error: unknown) {
const err = error as {
response?: { data?: { error?: string; message?: string } };
message?: string;
};
console.error("[LoginPaciente] Erro ao cadastrar:", {
error,
response: error?.response,
data: error?.response?.data,
response: err?.response,
data: err?.response?.data,
});
const errorMessage =
error?.response?.data?.error ||
error?.response?.data?.message ||
error?.message ||
err?.response?.data?.error ||
err?.response?.data?.message ||
err?.message ||
"Erro ao realizar cadastro";
toast.error(errorMessage);
} finally {
@ -158,68 +180,6 @@ const LoginPaciente: React.FC = () => {
}
};
// Login LOCAL: cria uma sessão de paciente sem chamar a API
const handleLoginLocal = async () => {
const email = formData.email.trim();
const senha = formData.senha;
console.log("[LoginPaciente] Login local - tentando com API primeiro");
// Tentar fazer login via API mesmo no modo "local"
setLoading(true);
try {
// Fazer login via API Supabase
await authService.login({
email: email,
password: senha,
});
console.log("[LoginPaciente] Login via API bem-sucedido!");
// Buscar dados do paciente da API
const pacientes = await patientService.list();
const paciente = pacientes.find((p: any) => p.email === email);
if (paciente) {
console.log(
"[LoginPaciente] Paciente encontrado na API:",
paciente.full_name
);
const ok = await loginPaciente({
id: paciente.id,
nome: paciente.full_name,
email: paciente.email,
});
if (ok) {
navigate("/acompanhamento");
} else {
toast.error("Erro ao processar login");
}
} else {
console.log(
"[LoginPaciente] Paciente não encontrado na API, usando dados locais"
);
const ok = await loginPaciente({
id: email,
nome: email.split("@")[0],
email: email,
});
if (ok) {
navigate("/acompanhamento");
} else {
toast.error("Erro ao processar login");
}
}
} catch (err) {
console.error("[LoginPaciente] Erro no login:", err);
toast.error("Erro ao fazer login");
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4 transition-colors">
<div className="max-w-md w-full">
@ -307,38 +267,25 @@ const LoginPaciente: React.FC = () => {
return;
}
try {
await authService.sendMagicLink(
formData.email,
"https://mediconnectbrasil.netlify.app/paciente/agendamento"
await authService.requestPasswordReset(formData.email);
toast.success(
"Email de recuperação enviado! Verifique sua caixa de entrada."
);
toast.success("Link de acesso enviado para seu email!");
} catch {
toast.error("Erro ao enviar link");
toast.error("Erro ao enviar email de recuperação");
}
}}
className="text-sm text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 hover:underline transition-colors"
className="text-sm text-blue-600 dark:text-blue-400 hover:underline transition-colors"
>
Esqueceu a senha?
</button>
</div>
</div>
{/** Botão original (remoto) comentado a pedido **/}
{/**
<button
type="submit"
disabled={loading}
className="w-full bg-gradient-to-r from-blue-700 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-800 hover:to-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
{loading ? "Entrando..." : "Entrar"}
</button>
**/}
<button
type="button"
onClick={handleLoginLocal}
disabled={loading}
className="w-full bg-gradient-to-r from-blue-700 to-blue-400 text-white py-3 px-4 rounded-lg font-medium hover:from-blue-800 hover:to-blue-500 hover:scale-105 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
className="w-full bg-gradient-to-r from-blue-600 to-cyan-500 dark:from-blue-700 dark:to-cyan-600 text-white py-3 rounded-lg font-semibold hover:from-blue-700 hover:to-cyan-600 dark:hover:from-blue-800 dark:hover:to-cyan-700 transition-all transform hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none shadow-md"
>
{loading ? "Entrando..." : "Entrar"}
</button>
@ -373,8 +320,9 @@ const LoginPaciente: React.FC = () => {
"Link de acesso enviado para seu email! Verifique sua caixa de entrada.",
{ duration: 6000 }
);
} catch (error: any) {
toast.error(error?.message || "Erro ao enviar link");
} catch (error: unknown) {
const err = error as { message?: string };
toast.error(err?.message || "Erro ao enviar link");
} finally {
setLoading(false);
}

View File

@ -3,7 +3,7 @@ import { Mail, Lock, Clipboard } from "lucide-react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
import { authService } from "../services";
import { authService, userService } from "../services";
const LoginSecretaria: React.FC = () => {
const [formData, setFormData] = useState({
@ -22,21 +22,73 @@ const LoginSecretaria: React.FC = () => {
try {
console.log("[LoginSecretaria] Fazendo login com email:", formData.email);
// Fazer login via API Supabase
const loginResponse = await authService.login({
email: formData.email,
password: formData.senha,
});
console.log("[LoginSecretaria] Login bem-sucedido!", loginResponse);
// Buscar informações completas do usuário (profile + roles)
const userInfo = await userService.getUserInfo();
console.log("[LoginSecretaria] UserInfo obtido:", userInfo);
const userName =
userInfo.profile?.full_name ||
loginResponse.user.email?.split("@")[0] ||
"Secretária";
const roles = userInfo.roles || [];
// Validar se tem permissão (admin, gestor ou secretaria)
// Secretária pode ser paciente também, mas não médica
const isAdmin = roles.includes("admin");
const isGestor = roles.includes("gestor");
const isSecretaria = roles.includes("secretaria");
const isMedico = roles.includes("medico");
if (!isAdmin && !isGestor && !isSecretaria) {
toast.error("Você não tem permissão para acessar esta área");
await authService.logout();
setLoading(false);
return;
}
// Secretária não pode ser médica (exceto se for admin/gestor)
if (isSecretaria && isMedico && !isAdmin && !isGestor) {
toast.error(
"Usuário com múltiplas funções incompatíveis. Entre em contato com o suporte."
);
await authService.logout();
setLoading(false);
return;
}
// Fazer login no contexto
const ok = await loginComEmailSenha(formData.email, formData.senha);
if (ok) {
console.log(
"[LoginSecretaria] Login bem-sucedido! Navegando para /painel-secretaria"
);
toast.success("Login realizado com sucesso!");
toast.success(`Bem-vinda, ${userName}!`);
navigate("/painel-secretaria");
} else {
console.error("[LoginSecretaria] loginComEmailSenha retornou false");
toast.error("Credenciais inválidas ou usuário sem permissão");
toast.error("Erro ao processar login");
}
} catch (error) {
} catch (error: unknown) {
console.error("[LoginSecretaria] Erro no login:", error);
toast.error("Erro ao fazer login. Verifique suas credenciais.");
const err = error as {
response?: { data?: { error_description?: string; message?: string } };
message?: string;
};
const errorMessage =
err?.response?.data?.error_description ||
err?.response?.data?.message ||
err?.message ||
"Erro ao fazer login. Verifique suas credenciais.";
toast.error(errorMessage);
} finally {
setLoading(false);
}
@ -117,13 +169,10 @@ const LoginSecretaria: React.FC = () => {
return;
}
try {
await authService.sendMagicLink(
formData.email,
"https://mediconnectbrasil.netlify.app/secretaria/painel"
);
toast.success("Link de acesso enviado para seu email!");
await authService.requestPasswordReset(formData.email);
toast.success("Email de recuperação enviado!");
} catch {
toast.error("Erro ao enviar link");
toast.error("Erro ao enviar email de recuperação");
}
}}
className="text-sm text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:underline transition-colors"

View File

@ -1,12 +1,14 @@
import { useState, useEffect } from "react";
import { Save } from "lucide-react";
import { Save, ArrowLeft } from "lucide-react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../hooks/useAuth";
import { patientService } from "../services";
import { AvatarUpload } from "../components/ui/AvatarUpload";
export default function PerfilPaciente() {
const { user } = useAuth();
const navigate = useNavigate();
const [isEditing, setIsEditing] = useState(false);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<
@ -47,13 +49,20 @@ export default function PerfilPaciente() {
}, [user?.id]);
const loadPatientData = async () => {
if (!user?.id) return;
if (!user?.id) {
console.error("[PerfilPaciente] Sem user.id:", user);
toast.error("Usuário não identificado");
return;
}
try {
setLoading(true);
const patient = await patientService.getById(user.id);
console.log("[PerfilPaciente] Buscando dados do paciente:", user.id);
try {
const patient = await patientService.getById(user.id);
console.log("[PerfilPaciente] Dados carregados:", patient);
if (patient) {
setFormData({
full_name: patient.full_name || "",
email: patient.email || "",
@ -72,11 +81,37 @@ export default function PerfilPaciente() {
weight_kg: patient.weight_kg?.toString() || "",
height_m: patient.height_m?.toString() || "",
});
// Patient type não tem avatar_url ainda
setAvatarUrl(undefined);
} catch {
console.warn(
"[PerfilPaciente] Paciente não encontrado na tabela patients, usando dados básicos do auth"
);
// Se não encontrar o paciente, usar dados básicos do usuário logado
setFormData({
full_name: user.nome || "",
email: user.email || "",
phone_mobile: "",
cpf: "",
birth_date: "",
sex: "",
street: "",
number: "",
complement: "",
neighborhood: "",
city: "",
state: "",
cep: "",
blood_type: "",
weight_kg: "",
height_m: "",
});
toast("Preencha seus dados para completar o cadastro", { icon: "" });
}
} catch (error) {
console.error("Erro ao carregar dados do paciente:", error);
console.error(
"[PerfilPaciente] Erro ao carregar dados do paciente:",
error
);
toast.error("Erro ao carregar dados do perfil");
} finally {
setLoading(false);
@ -94,11 +129,31 @@ export default function PerfilPaciente() {
: undefined,
height_m: formData.height_m ? parseFloat(formData.height_m) : undefined,
};
await patientService.update(user.id, dataToSave);
toast.success("Perfil atualizado com sucesso!");
try {
// Tentar atualizar primeiro
await patientService.update(user.id, dataToSave);
toast.success("Perfil atualizado com sucesso!");
} catch (updateError) {
console.warn(
"[PerfilPaciente] Erro ao atualizar, tentando criar:",
updateError
);
// Se falhar, tentar criar o paciente
try {
await patientService.create(dataToSave);
toast.success("Perfil criado com sucesso!");
} catch (createError) {
console.error("[PerfilPaciente] Erro ao criar perfil:", createError);
throw createError;
}
}
setIsEditing(false);
// Recarregar dados
await loadPatientData();
} catch (error) {
console.error("Erro ao salvar perfil:", error);
console.error("[PerfilPaciente] Erro ao salvar perfil:", error);
toast.error("Erro ao salvar perfil");
}
};
@ -134,20 +189,54 @@ export default function PerfilPaciente() {
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="w-16 h-16 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="text-center">
<div className="w-16 h-16 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-600 dark:text-gray-400">
Carregando perfil...
</p>
</div>
</div>
);
}
if (!user?.id) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-900">
<div className="text-center">
<p className="text-red-600 dark:text-red-400 mb-4">
Usuário não identificado
</p>
<button
onClick={() => navigate("/paciente")}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Fazer Login
</button>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50 py-8 px-4">
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8 px-4">
<div className="max-w-4xl mx-auto space-y-6">
{/* Botão Voltar */}
<button
onClick={() => navigate("/acompanhamento")}
className="flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 transition-colors mb-4"
>
<ArrowLeft className="w-5 h-5" />
Voltar para o Painel
</button>
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold text-gray-900">Meu Perfil</h1>
<p className="text-gray-600">
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">
Meu Perfil
</h1>
<p className="text-gray-600 dark:text-gray-400">
Gerencie suas informações pessoais e médicas
</p>
</div>
@ -165,7 +254,7 @@ export default function PerfilPaciente() {
setIsEditing(false);
loadPatientData();
}}
className="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors"
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
Cancelar
</button>
@ -181,8 +270,10 @@ export default function PerfilPaciente() {
</div>
{/* Avatar Card */}
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold mb-4">Foto de Perfil</h2>
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">
Foto de Perfil
</h2>
<div className="flex items-center gap-6">
<AvatarUpload
userId={user?.id}
@ -194,22 +285,26 @@ export default function PerfilPaciente() {
onAvatarUpdate={(url) => setAvatarUrl(url || undefined)}
/>
<div>
<p className="font-medium text-gray-900">{formData.full_name}</p>
<p className="text-gray-500">{formData.email}</p>
<p className="font-medium text-gray-900 dark:text-gray-100">
{formData.full_name || "Carregando..."}
</p>
<p className="text-gray-500 dark:text-gray-400">
{formData.email || "Sem email"}
</p>
</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white rounded-lg shadow">
<div className="border-b border-gray-200">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
<div className="border-b border-gray-200 dark:border-gray-700">
<nav className="flex -mb-px">
<button
onClick={() => setActiveTab("personal")}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === "personal"
? "border-blue-600 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
? "border-blue-600 text-blue-600 dark:text-blue-400"
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
}`}
>
Dados Pessoais
@ -218,8 +313,8 @@ export default function PerfilPaciente() {
onClick={() => setActiveTab("medical")}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === "medical"
? "border-blue-600 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
? "border-blue-600 text-blue-600 dark:text-blue-400"
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
}`}
>
Informações Médicas
@ -228,8 +323,8 @@ export default function PerfilPaciente() {
onClick={() => setActiveTab("security")}
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === "security"
? "border-blue-600 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
? "border-blue-600 text-blue-600 dark:text-blue-400"
: "border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-600"
}`}
>
Segurança
@ -242,16 +337,16 @@ export default function PerfilPaciente() {
{activeTab === "personal" && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-semibold mb-4">
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-gray-100">
Informações Pessoais
</h3>
<p className="text-sm text-gray-500 mb-4">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-4">
Mantenha seus dados atualizados
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Nome Completo
</label>
<input
@ -261,12 +356,12 @@ export default function PerfilPaciente() {
handleChange("full_name", e.target.value)
}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Email
</label>
<input
@ -274,12 +369,12 @@ export default function PerfilPaciente() {
value={formData.email}
onChange={(e) => handleChange("email", e.target.value)}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Telefone
</label>
<input
@ -289,12 +384,12 @@ export default function PerfilPaciente() {
handleChange("phone_mobile", e.target.value)
}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
CPF
</label>
<input
@ -306,7 +401,7 @@ export default function PerfilPaciente() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Data de Nascimento
</label>
<input
@ -316,19 +411,19 @@ export default function PerfilPaciente() {
handleChange("birth_date", e.target.value)
}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Sexo
</label>
<select
value={formData.sex}
onChange={(e) => handleChange("sex", e.target.value)}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option value="">Selecione</option>
<option value="M">Masculino</option>
@ -344,7 +439,7 @@ export default function PerfilPaciente() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="md:col-span-2">
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Rua
</label>
<input
@ -352,12 +447,12 @@ export default function PerfilPaciente() {
value={formData.street}
onChange={(e) => handleChange("street", e.target.value)}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Número
</label>
<input
@ -365,12 +460,12 @@ export default function PerfilPaciente() {
value={formData.number}
onChange={(e) => handleChange("number", e.target.value)}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Complemento
</label>
<input
@ -380,12 +475,12 @@ export default function PerfilPaciente() {
handleChange("complement", e.target.value)
}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Bairro
</label>
<input
@ -395,12 +490,12 @@ export default function PerfilPaciente() {
handleChange("neighborhood", e.target.value)
}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Cidade
</label>
<input
@ -408,12 +503,12 @@ export default function PerfilPaciente() {
value={formData.city}
onChange={(e) => handleChange("city", e.target.value)}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Estado
</label>
<input
@ -422,12 +517,12 @@ export default function PerfilPaciente() {
onChange={(e) => handleChange("state", e.target.value)}
disabled={!isEditing}
maxLength={2}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
CEP
</label>
<input
@ -435,7 +530,7 @@ export default function PerfilPaciente() {
value={formData.cep}
onChange={(e) => handleChange("cep", e.target.value)}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
</div>
@ -456,7 +551,7 @@ export default function PerfilPaciente() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Tipo Sanguíneo
</label>
<select
@ -465,7 +560,7 @@ export default function PerfilPaciente() {
handleChange("blood_type", e.target.value)
}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option value="">Selecione</option>
<option value="A+">A+</option>
@ -479,7 +574,7 @@ export default function PerfilPaciente() {
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Peso (kg)
</label>
<input
@ -490,12 +585,12 @@ export default function PerfilPaciente() {
handleChange("weight_kg", e.target.value)
}
disabled={!isEditing}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Altura (m)
</label>
<input
@ -507,7 +602,7 @@ export default function PerfilPaciente() {
}
disabled={!isEditing}
placeholder="Ex: 1.75"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 disabled:text-gray-500"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
</div>
@ -526,7 +621,7 @@ export default function PerfilPaciente() {
<div className="max-w-md space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Senha Atual
</label>
<input
@ -544,7 +639,7 @@ export default function PerfilPaciente() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Nova Senha
</label>
<input
@ -562,7 +657,7 @@ export default function PerfilPaciente() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Confirmar Nova Senha
</label>
<input

View File

@ -0,0 +1,266 @@
import React, { useState, useEffect } from "react";
import { Lock, Eye, EyeOff, CheckCircle } from "lucide-react";
import toast from "react-hot-toast";
import { useNavigate } from "react-router-dom";
import { authService } from "../services";
const ResetPassword: React.FC = () => {
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [accessToken, setAccessToken] = useState<string | null>(null);
const navigate = useNavigate();
useEffect(() => {
// Extrair access_token do hash da URL
const hash = window.location.hash;
console.log("[ResetPassword] Hash completo:", hash);
if (hash) {
const params = new URLSearchParams(hash.substring(1));
const token = params.get("access_token");
const type = params.get("type");
console.log(
"[ResetPassword] Token extraído:",
token ? token.substring(0, 20) + "..." : "null"
);
console.log("[ResetPassword] Type:", type);
if (token) {
setAccessToken(token);
console.log(
"[ResetPassword] ✅ Token de recuperação detectado e armazenado"
);
} else {
console.error("[ResetPassword] ❌ Token não encontrado no hash");
toast.error("Link de recuperação inválido ou expirado");
setTimeout(() => navigate("/"), 3000);
}
} else {
console.error("[ResetPassword] ❌ Nenhum hash encontrado na URL");
console.log("[ResetPassword] URL completa:", window.location.href);
toast.error("Link de recuperação inválido");
setTimeout(() => navigate("/"), 3000);
}
}, [navigate]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validações
if (!password.trim()) {
toast.error("Digite a nova senha");
return;
}
if (password.length < 6) {
toast.error("A senha deve ter pelo menos 6 caracteres");
return;
}
if (password !== confirmPassword) {
toast.error("As senhas não coincidem");
return;
}
if (!accessToken) {
toast.error("Token de recuperação não encontrado");
return;
}
setLoading(true);
try {
console.log("[ResetPassword] Atualizando senha...");
// Atualizar senha usando o token de recuperação
await authService.updatePassword(accessToken, password);
console.log("[ResetPassword] Senha atualizada com sucesso!");
toast.success("Senha atualizada com sucesso!");
// Limpar formulário
setPassword("");
setConfirmPassword("");
// Redirecionar para login após 2 segundos
setTimeout(() => {
navigate("/");
}, 2000);
} catch (error: unknown) {
console.error("[ResetPassword] Erro ao atualizar senha:", error);
const err = error as {
response?: { data?: { error_description?: string; message?: string } };
message?: string;
};
const errorMessage =
err?.response?.data?.error_description ||
err?.response?.data?.message ||
err?.message ||
"Erro ao atualizar senha. Tente novamente.";
toast.error(errorMessage);
} finally {
setLoading(false);
}
};
// Validações em tempo real
const hasMinLength = password.length >= 6;
const passwordsMatch = password === confirmPassword && confirmPassword !== "";
// Se não tiver token ainda, mostrar loading
if (!accessToken) {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
<p className="text-gray-600 dark:text-gray-400">
Verificando link de recuperação...
</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-white dark:from-gray-900 dark:to-gray-950 flex items-center justify-center p-4 transition-colors">
<div className="max-w-md w-full">
{/* Header */}
<div className="text-center mb-8">
<div className="bg-gradient-to-r from-blue-600 to-blue-400 dark:from-blue-700 dark:to-blue-500 w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-4 shadow-md">
<Lock className="w-8 h-8 text-white" />
</div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100 mb-2">
Redefinir Senha
</h1>
<p className="text-gray-600 dark:text-gray-400">
Digite sua nova senha abaixo
</p>
</div>
{/* Formulário */}
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8 border border-transparent dark:border-gray-700 transition-colors">
<form onSubmit={handleSubmit} className="space-y-6">
{/* Nova Senha */}
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Nova Senha
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
id="password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-10 pr-12 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-gray-100"
placeholder="Digite sua nova senha"
required
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showPassword ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</button>
</div>
</div>
{/* Confirmar Senha */}
<div>
<label
htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2"
>
Confirmar Nova Senha
</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
id="confirmPassword"
type={showConfirmPassword ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full pl-10 pr-12 py-3 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-700 dark:text-gray-100"
placeholder="Confirme sua nova senha"
required
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showConfirmPassword ? (
<EyeOff className="w-5 h-5" />
) : (
<Eye className="w-5 h-5" />
)}
</button>
</div>
</div>
{/* Indicadores de Validação */}
{password && (
<div className="space-y-2 text-sm">
<div
className={`flex items-center gap-2 ${
hasMinLength ? "text-green-600" : "text-gray-500"
}`}
>
<CheckCircle className="w-4 h-4" />
<span>Mínimo de 6 caracteres</span>
</div>
{confirmPassword && (
<div
className={`flex items-center gap-2 ${
passwordsMatch ? "text-green-600" : "text-red-600"
}`}
>
<CheckCircle className="w-4 h-4" />
<span>
{passwordsMatch
? "As senhas coincidem"
: "As senhas não coincidem"}
</span>
</div>
)}
</div>
)}
{/* Botão Submit */}
<button
type="submit"
disabled={loading || !hasMinLength || !passwordsMatch}
className="w-full bg-blue-600 text-white py-3 px-4 rounded-lg font-medium hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200"
>
{loading ? "Atualizando..." : "Redefinir Senha"}
</button>
</form>
{/* Link para voltar */}
<div className="mt-6 text-center">
<button
onClick={() => navigate("/")}
className="text-sm text-blue-600 dark:text-blue-400 hover:underline"
>
Voltar para o login
</button>
</div>
</div>
</div>
</div>
);
};
export default ResetPassword;

View File

@ -1,6 +1,6 @@
/**
* Cliente HTTP usando Axios
* Todas as requisições passam pelas Netlify Functions
* Chamadas diretas ao Supabase
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
@ -11,10 +11,11 @@ class ApiClient {
constructor() {
this.client = axios.create({
baseURL: API_CONFIG.BASE_URL,
baseURL: API_CONFIG.REST_URL,
timeout: API_CONFIG.TIMEOUT,
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
});
@ -24,10 +25,10 @@ class ApiClient {
private setupInterceptors() {
// Request interceptor - adiciona token automaticamente
this.client.interceptors.request.use(
(config: any) => {
(config) => {
// Não adicionar token se a flag _skipAuth estiver presente
if (config._skipAuth) {
delete config._skipAuth;
if ((config as any)._skipAuth) {
delete (config as any)._skipAuth;
return config;
}
@ -88,9 +89,20 @@ class ApiClient {
if (refreshToken) {
console.log("[ApiClient] Refresh token encontrado, renovando...");
const response = await this.client.post("/auth-refresh", {
refresh_token: refreshToken,
});
// Chama Supabase diretamente para renovar token
const response = await axios.post(
`${API_CONFIG.AUTH_URL}/token?grant_type=refresh_token`,
{
refresh_token: refreshToken,
},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
}
);
const {
access_token,

View File

@ -1,18 +1,25 @@
/**
* Configuração da API
* Frontend sempre chama Netlify Functions (não o Supabase direto)
* Frontend chama Supabase diretamente
*/
// Em desenvolvimento, Netlify Dev roda na porta 8888
// Em produção, usa URL completa do Netlify
const isDevelopment = import.meta.env.DEV;
const BASE_URL = isDevelopment
? "http://localhost:8888/.netlify/functions"
: "https://mediconnectbrasil.netlify.app/.netlify/functions";
export const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
export const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
// URL base do app
export const APP_URL = "https://mediconnectbrasil.app";
export const API_CONFIG = {
// Base URL aponta para suas Netlify Functions
BASE_URL,
// Base URLs do Supabase
SUPABASE_URL,
SUPABASE_ANON_KEY,
AUTH_URL: `${SUPABASE_URL}/auth/v1`,
REST_URL: `${SUPABASE_URL}/rest/v1`,
FUNCTIONS_URL: `${SUPABASE_URL}/functions/v1`,
// URL base do app
APP_URL,
// Timeout padrão (30 segundos)
TIMEOUT: 30000,

View File

@ -72,8 +72,13 @@ class AppointmentService {
* Busca agendamento por ID
*/
async getById(id: string): Promise<Appointment> {
const response = await apiClient.get<Appointment>(`${this.basePath}/${id}`);
return response.data;
const response = await apiClient.get<Appointment[]>(
`${this.basePath}?id=eq.${id}`
);
if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Agendamento não encontrado");
}
/**
@ -89,18 +94,21 @@ class AppointmentService {
* Atualiza agendamento existente
*/
async update(id: string, data: UpdateAppointmentInput): Promise<Appointment> {
const response = await apiClient.patch<Appointment>(
`${this.basePath}/${id}`,
const response = await apiClient.patch<Appointment[]>(
`${this.basePath}?id=eq.${id}`,
data
);
return response.data;
if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Agendamento não encontrado");
}
/**
* Deleta agendamento
*/
async delete(id: string): Promise<void> {
await apiClient.delete(`${this.basePath}/${id}`);
await apiClient.delete(`${this.basePath}?id=eq.${id}`);
}
}

View File

@ -1,9 +1,9 @@
/**
* Serviço de Autenticação (Frontend)
* Chama as Netlify Functions, não o Supabase direto
* Chama Supabase diretamente
*/
import { apiClient } from "../api/client";
import axios from "axios";
import { API_CONFIG } from "../api/config";
import type {
LoginInput,
@ -18,9 +18,78 @@ class AuthService {
*/
async login(credentials: LoginInput): Promise<LoginResponse> {
try {
const response = await apiClient.post<LoginResponse>(
"/auth-login",
credentials
console.log("[authService] Tentando login com:", credentials.email);
const response = await axios.post<LoginResponse>(
`${API_CONFIG.AUTH_URL}/token?grant_type=password`,
{
email: credentials.email,
password: credentials.password,
},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
}
);
console.log("[authService] Login bem-sucedido:", response.data);
// Salva tokens e user no localStorage
if (response.data.access_token) {
localStorage.setItem(
API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN,
response.data.access_token
);
localStorage.setItem(
API_CONFIG.STORAGE_KEYS.REFRESH_TOKEN,
response.data.refresh_token
);
localStorage.setItem(
API_CONFIG.STORAGE_KEYS.USER,
JSON.stringify(response.data.user)
);
}
return response.data;
} catch (error: any) {
console.error("[authService] Erro no login:", error);
console.error("[authService] Response data:", error.response?.data);
console.error("[authService] Response status:", error.response?.status);
throw error;
}
}
/**
* Registro público (signup) com email e senha
* POST /auth/v1/signup
* Não requer autenticação - permite auto-registro
*/
async signup(data: {
email: string;
password: string;
full_name: string;
phone?: string;
}): Promise<LoginResponse> {
try {
const response = await axios.post<LoginResponse>(
`${API_CONFIG.AUTH_URL}/signup`,
{
email: data.email,
password: data.password,
options: {
data: {
full_name: data.full_name,
phone: data.phone,
},
},
},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
}
);
// Salva tokens e user no localStorage
@ -41,7 +110,7 @@ class AuthService {
return response.data;
} catch (error) {
console.error("Erro no login:", error);
console.error("Erro no signup:", error);
throw error;
}
}
@ -55,14 +124,27 @@ class AuthService {
redirectUrl?: string
): Promise<{ success: boolean; message: string }> {
try {
const response = await apiClient.post<{
success: boolean;
message: string;
}>("/auth-magic-link", {
email,
redirect_url: redirectUrl,
});
return response.data;
await axios.post(
`${API_CONFIG.AUTH_URL}/otp`,
{
email,
options: {
emailRedirectTo:
redirectUrl || `${API_CONFIG.APP_URL}/auth/callback`,
},
},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
}
);
return {
success: true,
message: "Magic link enviado com sucesso",
};
} catch (error) {
console.error("Erro ao enviar magic link:", error);
throw error;
@ -71,34 +153,94 @@ class AuthService {
/**
* Solicita reset de senha via email (público)
* POST /request-password-reset
* POST /auth/v1/recover
*/
async requestPasswordReset(
email: string,
redirectUrl?: string
): Promise<{ success: boolean; message: string }> {
try {
const response = await (apiClient as any).postPublic<{
success: boolean;
message: string;
}>("/request-password-reset", {
email,
redirect_url: redirectUrl,
});
return response.data;
await axios.post(
`${API_CONFIG.AUTH_URL}/recover`,
{
email,
options: {
redirectTo: redirectUrl || `${API_CONFIG.APP_URL}/reset-password`,
},
},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
}
);
return {
success: true,
message:
"Email de recuperação de senha enviado com sucesso. Verifique sua caixa de entrada.",
};
} catch (error) {
console.error("Erro ao solicitar reset de senha:", error);
throw error;
}
}
/**
* Atualiza a senha do usuário usando o token de recuperação
* PUT /auth/v1/user
*/
async updatePassword(
accessToken: string,
newPassword: string
): Promise<{ success: boolean; message: string }> {
try {
await axios.put(
`${API_CONFIG.AUTH_URL}/user`,
{
password: newPassword,
},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
Authorization: `Bearer ${accessToken}`,
},
}
);
return {
success: true,
message: "Senha atualizada com sucesso",
};
} catch (error) {
console.error("Erro ao atualizar senha:", error);
throw error;
}
}
/**
* Faz logout (invalida sessão no servidor e limpa localStorage)
*/
async logout(): Promise<void> {
try {
// Chama API para invalidar sessão no servidor
await apiClient.post("/auth-logout");
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
if (token) {
// Chama API para invalidar sessão no servidor
await axios.post(
`${API_CONFIG.AUTH_URL}/logout`,
{},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
},
}
);
}
} catch (error) {
console.error("Erro ao invalidar sessão no servidor:", error);
// Continua mesmo com erro, para garantir limpeza local
@ -151,10 +293,16 @@ class AuthService {
throw new Error("Refresh token não encontrado");
}
const response = await apiClient.post<RefreshTokenResponse>(
"/auth-refresh",
const response = await axios.post<RefreshTokenResponse>(
`${API_CONFIG.AUTH_URL}/token?grant_type=refresh_token`,
{
refresh_token: refreshToken,
},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
}
);

View File

@ -29,24 +29,11 @@ class AvailabilityService {
* Cria uma nova configuração de disponibilidade
*/
async create(data: CreateAvailabilityInput): Promise<DoctorAvailability> {
console.log("[availabilityService.create] 📤 Enviando dados:", JSON.stringify(data, null, 2));
try {
const response = await apiClient.post<DoctorAvailability>(
this.basePath,
data
);
console.log("[availabilityService.create] ✅ Resposta:", response.data);
return response.data;
} catch (error: any) {
console.error("[availabilityService.create] ❌ Erro:", {
message: error?.message,
response: error?.response?.data,
status: error?.response?.status,
data: data
});
throw error;
}
const response = await apiClient.post<DoctorAvailability>(
this.basePath,
data
);
return response.data;
}
/**

View File

@ -53,8 +53,11 @@ class DoctorService {
*/
async getById(id: string): Promise<Doctor> {
try {
const response = await apiClient.get<Doctor>(`/doctors/${id}`);
return response.data;
const response = await apiClient.get<Doctor[]>(`/doctors?id=eq.${id}`);
if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Médico não encontrado");
} catch (error) {
console.error("Erro ao buscar médico:", error);
throw error;
@ -79,8 +82,14 @@ class DoctorService {
*/
async update(id: string, data: UpdateDoctorInput): Promise<Doctor> {
try {
const response = await apiClient.patch<Doctor>(`/doctors/${id}`, data);
return response.data;
const response = await apiClient.patch<Doctor[]>(
`/doctors?id=eq.${id}`,
data
);
if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Médico não encontrado");
} catch (error) {
console.error("Erro ao atualizar médico:", error);
throw error;
@ -92,7 +101,7 @@ class DoctorService {
*/
async delete(id: string): Promise<void> {
try {
await apiClient.delete(`/doctors/${id}`);
await apiClient.delete(`/doctors?id=eq.${id}`);
} catch (error) {
console.error("Erro ao deletar médico:", error);
throw error;

View File

@ -36,22 +36,12 @@ class PatientService {
const queryString = params.toString();
const url = queryString ? `/patients?${queryString}` : "/patients";
console.log(`[patientService.list] 📤 Chamando: ${url}`);
const response = await apiClient.get<Patient[]>(url);
console.log(`[patientService.list] ✅ Resposta:`, {
status: response.status,
total: Array.isArray(response.data) ? response.data.length : 0,
data: response.data
});
return response.data;
} catch (error: any) {
// Silenciar erro 401 (não autenticado) - é esperado em páginas públicas
if (error?.response?.status !== 401) {
console.error("[patientService.list] ❌ Erro ao listar pacientes:", {
message: error?.message,
response: error?.response?.data,
status: error?.response?.status
});
console.error("Erro ao listar pacientes:", error);
}
throw error;
}
@ -62,10 +52,22 @@ class PatientService {
*/
async getById(id: string): Promise<Patient> {
try {
const response = await apiClient.get<Patient>(`/patients/${id}`);
return response.data;
console.log("[patientService] Buscando paciente por ID:", id);
const response = await apiClient.get<Patient[]>(`/patients?id=eq.${id}`);
console.log("[patientService] Response data:", response.data);
console.log("[patientService] Response length:", response.data?.length);
if (response.data && response.data.length > 0) {
console.log("[patientService] Paciente encontrado:", response.data[0]);
return response.data[0];
}
console.warn(
"[patientService] Array vazio - paciente não existe na tabela patients"
);
throw new Error("Paciente não encontrado");
} catch (error) {
console.error("Erro ao buscar paciente:", error);
console.error("[patientService] Erro ao buscar paciente:", error);
throw error;
}
}
@ -88,8 +90,14 @@ class PatientService {
*/
async update(id: string, data: UpdatePatientInput): Promise<Patient> {
try {
const response = await apiClient.patch<Patient>(`/patients/${id}`, data);
return response.data;
const response = await apiClient.patch<Patient[]>(
`/patients?id=eq.${id}`,
data
);
if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Paciente não encontrado");
} catch (error) {
console.error("Erro ao atualizar paciente:", error);
throw error;
@ -101,7 +109,7 @@ class PatientService {
*/
async delete(id: string): Promise<void> {
try {
await apiClient.delete(`/patients/${id}`);
await apiClient.delete(`/patients?id=eq.${id}`);
} catch (error) {
console.error("Erro ao deletar paciente:", error);
throw error;
@ -114,27 +122,22 @@ class PatientService {
*/
async register(data: RegisterPatientInput): Promise<RegisterPatientResponse> {
try {
console.log("[patientService.register] 📤 Enviando dados:", JSON.stringify(data, null, 2));
console.log("[patientService.register] Enviando dados:", data);
// Usa postPublic para não enviar token de autenticação
const response = await (apiClient as any).postPublic("/register-patient", data) as any;
const response = await (
apiClient as any
).postPublic<RegisterPatientResponse>("/register-patient", data);
console.log("[patientService.register] ✅ Resposta recebida:", response.data);
console.log("[patientService.register] Resposta:", response.data);
return response.data;
} catch (error: any) {
console.error("[patientService.register] Erro completo:", {
console.error("[patientService.register] Erro completo:", {
message: error.message,
response: error.response?.data,
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
});
// Re-lança o erro com mais informações
const errorMessage = error.response?.data?.error || error.response?.data?.message || error.message;
const enhancedError = new Error(errorMessage);
(enhancedError as any).response = error.response;
throw enhancedError;
throw error;
}
}
}

View File

@ -4,7 +4,6 @@
export interface Patient {
id?: string;
user_id?: string;
full_name: string;
cpf: string;
email: string;
@ -29,7 +28,6 @@ export interface Patient {
}
export interface CreatePatientInput {
user_id?: string;
full_name: string;
cpf: string;
email: string;

View File

@ -35,8 +35,13 @@ class ReportService {
* Busca relatório por ID
*/
async getById(id: string): Promise<Report> {
const response = await apiClient.get<Report>(`${this.basePath}/${id}`);
return response.data;
const response = await apiClient.get<Report[]>(
`${this.basePath}?id=eq.${id}`
);
if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Relatório não encontrado");
}
/**
@ -53,11 +58,14 @@ class ReportService {
* Nota: order_number não pode ser modificado
*/
async update(id: string, data: UpdateReportInput): Promise<Report> {
const response = await apiClient.patch<Report>(
`${this.basePath}/${id}`,
const response = await apiClient.patch<Report[]>(
`${this.basePath}?id=eq.${id}`,
data
);
return response.data;
if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Relatório não encontrado");
}
}

View File

@ -55,6 +55,7 @@ export interface CreateUserInput {
email: string;
full_name: string;
phone?: string | null;
password?: string; // Senha para registro direto (opcional - se não fornecida, envia magic link)
role: UserRole; // Agora é obrigatório
create_patient_record?: boolean; // Novo campo opcional
cpf?: string; // Obrigatório se create_patient_record=true

View File

@ -2,7 +2,9 @@
* Serviço de Usuários
*/
import axios from "axios";
import { apiClient } from "../api/client";
import { API_CONFIG } from "../api/config";
import type {
UserRoleRecord,
UserInfo,
@ -20,7 +22,7 @@ class UserService {
* Lista roles de usuários
*/
async listRoles(): Promise<UserRoleRecord[]> {
const response = await apiClient.get<UserRoleRecord[]>("/user-roles");
const response = await apiClient.get<UserRoleRecord[]>("/user_roles");
return response.data;
}
@ -29,7 +31,24 @@ class UserService {
* Inclui perfil, roles e permissões calculadas
*/
async getUserInfo(): Promise<UserInfo> {
const response = await apiClient.get<UserInfo>("/user-info");
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
if (!token) {
throw new Error("Token não encontrado");
}
const response = await axios.post<UserInfo>(
`${API_CONFIG.FUNCTIONS_URL}/user-info`,
{},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
}
@ -37,29 +56,84 @@ class UserService {
* Obtém dados básicos do usuário autenticado
*/
async getCurrentUser(): Promise<User> {
const response = await apiClient.get<User>("/auth-user");
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
if (!token) {
throw new Error("Token não encontrado");
}
const response = await axios.get<User>(`${API_CONFIG.AUTH_URL}/user`, {
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
},
});
return response.data;
}
/**
* Cria novo usuário no sistema
* Pode ser chamado COM ou SEM autenticação:
* - SEM autenticação: auto-registro público (médico, paciente)
* - COM autenticação: criação por admin/secretária
* Envia magic link automaticamente para o email
* - SEM autenticação: usa signup nativo (/auth/v1/signup) - PÚBLICO
* - COM autenticação: usa Edge Function (/functions/v1/create-user) - ADMIN
*/
async createUser(
data: CreateUserInput,
isPublicRegistration: boolean = true
): Promise<CreateUserResponse> {
// Se for registro público, usa postPublic (sem token)
// Se for criação por admin, usa post normal (com token)
const response = isPublicRegistration
? await (apiClient as any).postPublic<CreateUserResponse>(
"/create-user",
data
)
: await apiClient.post<CreateUserResponse>("/create-user", data);
// Registro público: usar endpoint nativo do Supabase
if (isPublicRegistration) {
const response = await axios.post<{
user: User;
session: { access_token: string; refresh_token: string };
}>(
`${API_CONFIG.AUTH_URL}/signup`,
{
email: data.email,
password: data.password,
options: {
data: {
full_name: data.full_name,
phone: data.phone,
},
},
},
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
},
}
);
return {
success: true,
user: {
id: response.data.user.id,
email: response.data.user.email,
full_name: data.full_name,
phone: data.phone || null,
roles: [data.role],
},
message: "Usuário criado com sucesso",
};
}
// Criação por admin: usar Edge Function
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
const response = await axios.post<CreateUserResponse>(
`${API_CONFIG.FUNCTIONS_URL}/create-user`,
data,
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
}
@ -69,7 +143,7 @@ class UserService {
* Requer permissão de admin
*/
async addUserRole(userId: string, role: string): Promise<UserRoleRecord> {
const response = await apiClient.post<UserRoleRecord>("/user-roles", {
const response = await apiClient.post<UserRoleRecord>("/user_roles", {
user_id: userId,
role: role,
});
@ -81,7 +155,7 @@ class UserService {
* Requer permissão de admin
*/
async removeUserRole(userId: string, role: string): Promise<void> {
await apiClient.delete(`/user-roles?user_id=${userId}&role=${role}`);
await apiClient.delete(`/user_roles?user_id=eq.${userId}&role=eq.${role}`);
}
/**
@ -92,9 +166,18 @@ class UserService {
* Use quando tiver TODOS os dados do médico
*/
async createDoctor(data: CreateDoctorInput): Promise<CreateDoctorResponse> {
const response = await apiClient.post<CreateDoctorResponse>(
"/create-doctor",
data
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
const response = await axios.post<CreateDoctorResponse>(
`${API_CONFIG.FUNCTIONS_URL}/create-doctor`,
data,
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
}
@ -109,15 +192,25 @@ class UserService {
async createPatient(
data: CreatePatientInput
): Promise<CreatePatientResponse> {
const response = await apiClient.post<CreatePatientResponse>(
"/create-patient",
data
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
const response = await axios.post<CreatePatientResponse>(
`${API_CONFIG.FUNCTIONS_URL}/create-patient`,
data,
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
}
/**
* Cria usuário com email e senha (alternativa ao Magic Link)
* POST /functions/v1/create-user-with-password
* Requer permissão de admin, gestor ou secretaria
* O usuário precisa confirmar o email antes de fazer login
*/
@ -137,15 +230,31 @@ class UserService {
email: string;
full_name: string;
roles: string[];
email_confirmed_at: string | null;
patient_id?: string;
};
message: string;
}> {
const response = await apiClient.post("/create-user-with-password", data);
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
const response = await axios.post(
`${API_CONFIG.FUNCTIONS_URL}/create-user-with-password`,
data,
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
}
/**
* Deleta usuário permanentemente (Hard Delete)
* POST /delete-user
* OPERAÇÃO IRREVERSÍVEL! Use apenas em desenvolvimento/QA
* Requer permissão de admin ou gestor
* Deleta em cascata: profiles, user_roles, doctors, patients, etc.
@ -155,7 +264,51 @@ class UserService {
message: string;
userId: string;
}> {
const response = await apiClient.post("/delete-user", { userId });
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
const response = await axios.post<{
success: boolean;
message: string;
userId: string;
}>(
`${API_CONFIG.FUNCTIONS_URL}/delete-user`,
{ userId },
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
}
/**
* Obtém informações de usuário por ID
* POST /functions/v1/user-info-by-id
* Requer permissão de admin ou gestor
*/
async getUserInfoById(userId: string): Promise<UserInfo> {
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
if (!token) {
throw new Error("Token não encontrado");
}
const response = await axios.post<UserInfo>(
`${API_CONFIG.FUNCTIONS_URL}/user-info-by-id`,
{ user_id: userId },
{
headers: {
"Content-Type": "application/json",
apikey: API_CONFIG.SUPABASE_ANON_KEY,
Authorization: `Bearer ${token}`,
},
}
);
return response.data;
}
}

View File

@ -1,77 +0,0 @@
/**
* Utilitários de validação
*/
/**
* Valida se um CPF é válido (algoritmo oficial)
*/
export function validarCPF(cpf: string): boolean {
// Remove caracteres não numéricos
const cpfLimpo = cpf.replace(/\D/g, "");
// Verifica se tem 11 dígitos
if (cpfLimpo.length !== 11) {
return false;
}
// Verifica se todos os dígitos são iguais (ex: 111.111.111-11)
if (/^(\d)\1{10}$/.test(cpfLimpo)) {
return false;
}
// Validação do primeiro dígito verificador
let soma = 0;
for (let i = 0; i < 9; i++) {
soma += parseInt(cpfLimpo.charAt(i)) * (10 - i);
}
let resto = (soma * 10) % 11;
if (resto === 10 || resto === 11) resto = 0;
if (resto !== parseInt(cpfLimpo.charAt(9))) {
return false;
}
// Validação do segundo dígito verificador
soma = 0;
for (let i = 0; i < 10; i++) {
soma += parseInt(cpfLimpo.charAt(i)) * (11 - i);
}
resto = (soma * 10) % 11;
if (resto === 10 || resto === 11) resto = 0;
if (resto !== parseInt(cpfLimpo.charAt(10))) {
return false;
}
return true;
}
/**
* Formata CPF para exibição (000.000.000-00)
*/
export function formatarCPF(cpf: string): string {
const cpfLimpo = cpf.replace(/\D/g, "");
if (cpfLimpo.length !== 11) {
return cpf;
}
return cpfLimpo.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, "$1.$2.$3-$4");
}
/**
* Formata telefone para exibição
*/
export function formatarTelefone(telefone: string): string {
const telefoneLimpo = telefone.replace(/\D/g, "");
// +55 11 99999-9999
if (telefoneLimpo.length === 13) {
return telefoneLimpo.replace(/(\d{2})(\d{2})(\d{5})(\d{4})/, "+$1 $2 $3-$4");
}
// +55 11 9999-9999
if (telefoneLimpo.length === 12) {
return telefoneLimpo.replace(/(\d{2})(\d{2})(\d{4})(\d{4})/, "+$1 $2 $3-$4");
}
return telefone;
}

View File

@ -44,6 +44,16 @@ export default {
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
transitionDuration: {
2000: "2000ms",
},
animation: {
bounce: "bounce 1s infinite",
},
animationDelay: {
100: "100ms",
200: "200ms",
},
},
},
plugins: [],

View File

@ -0,0 +1,55 @@
import axios from "axios";
const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const SUPABASE_ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
async function testPasswordRecovery() {
console.log("\n=== TESTE DE RECUPERAÇÃO DE SENHA ===\n");
const testEmail = "fernando.pirichowski@souunit.com.br";
try {
console.log("📧 Enviando email de recuperação para:", testEmail);
const response = await axios.post(
`${SUPABASE_URL}/auth/v1/recover`,
{
email: testEmail,
options: {
redirectTo: "https://mediconnectbrasil.app/reset-password",
},
},
{
headers: {
"Content-Type": "application/json",
apikey: SUPABASE_ANON_KEY,
},
}
);
console.log("✅ Email de recuperação enviado com sucesso!");
console.log("Status:", response.status);
console.log("Response:", JSON.stringify(response.data, null, 2));
console.log("\n📬 Verifique o email:", testEmail);
console.log(
"🔗 O link redirecionará para: https://mediconnectbrasil.app/reset-password"
);
console.log("\n💡 O link virá no formato:");
console.log(
" https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/verify?token=...&type=recovery&redirect_to=https://mediconnectbrasil.app/reset-password"
);
console.log("\n💡 Depois do clique, será redirecionado para:");
console.log(
" https://mediconnectbrasil.app/reset-password#access_token=...&refresh_token=..."
);
} catch (error) {
console.log("❌ Erro ao enviar email de recuperação:");
console.log("Status:", error.response?.status);
console.log("Error:", error.response?.data || error.message);
}
console.log("\n=== TESTE CONCLUÍDO ===\n");
}
testPasswordRecovery();

View File

@ -0,0 +1,54 @@
const axios = require("axios");
const ANON_KEY =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
(async () => {
try {
console.log("\n=== TESTE DE RECUPERAÇÃO COM REDIRECT_TO CORRETO ===\n");
const email = "fernando.pirichowski@souunit.com.br";
const redirectTo = "https://mediconnectbrasil.app/reset-password";
console.log(`📧 Enviando email de recuperação para: ${email}`);
console.log(`🔗 Redirect URL: ${redirectTo}\n`);
const response = await axios.post(
`${BASE_URL}/auth/v1/recover`,
{
email: email,
options: {
redirectTo: redirectTo,
},
},
{
headers: {
"Content-Type": "application/json",
apikey: ANON_KEY,
},
}
);
console.log("✅ Email de recuperação enviado com sucesso!");
console.log("Status:", response.status);
console.log("Response:", JSON.stringify(response.data, null, 2));
console.log("\n📬 Verifique o email:", email);
console.log("🔗 O link DEVE redirecionar para:", redirectTo);
console.log("\n💡 IMPORTANTE: Se ainda vier o link errado, você precisa:");
console.log(" 1. Acessar o painel do Supabase");
console.log(" 2. Ir em Authentication > URL Configuration");
console.log(
' 3. Atualizar o "Site URL" para: https://mediconnectbrasil.app'
);
console.log(
' 4. Adicionar https://mediconnectbrasil.app/* nos "Redirect URLs"'
);
console.log("\n=== TESTE CONCLUÍDO ===\n");
} catch (error) {
console.error("❌ Erro ao enviar email de recuperação:");
console.error(error.response?.data || error.message);
}
})();