Atualizar
This commit is contained in:
parent
2d8fcb5b4a
commit
eae5e8cb92
76
MEDICONNECT 2/add-fernando-patient.cjs
Normal file
76
MEDICONNECT 2/add-fernando-patient.cjs
Normal 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);
|
||||
}
|
||||
})();
|
||||
58
MEDICONNECT 2/check-fernando.js
Normal file
58
MEDICONNECT 2/check-fernando.js
Normal 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);
|
||||
}
|
||||
})();
|
||||
87
MEDICONNECT 2/create-fernando.cjs
Normal file
87
MEDICONNECT 2/create-fernando.cjs
Normal 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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
@ -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" }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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" }),
|
||||
};
|
||||
};
|
||||
@ -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,
|
||||
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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" }),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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": "*",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -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": "*",
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -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",
|
||||
}),
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
/* /index.html 200
|
||||
BIN
MEDICONNECT 2/public/logo.PNG
Normal file
BIN
MEDICONNECT 2/public/logo.PNG
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 472 KiB |
134
MEDICONNECT 2/scripts/cleanup-users.js
Normal file
134
MEDICONNECT 2/scripts/cleanup-users.js
Normal 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);
|
||||
275
MEDICONNECT 2/scripts/manage-users.js
Normal file
275
MEDICONNECT 2/scripts/manage-users.js
Normal 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);
|
||||
86
MEDICONNECT 2/search-fernando.cjs
Normal file
86
MEDICONNECT 2/search-fernando.cjs
Normal 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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
@ -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 />} />
|
||||
|
||||
277
MEDICONNECT 2/src/components/Chatbot.tsx
Normal file
277
MEDICONNECT 2/src/components/Chatbot.tsx
Normal 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;
|
||||
151
MEDICONNECT 2/src/components/HeroBanner.tsx
Normal file
151
MEDICONNECT 2/src/components/HeroBanner.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
BIN
MEDICONNECT 2/src/components/images/medico1.jpg
Normal file
BIN
MEDICONNECT 2/src/components/images/medico1.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
BIN
MEDICONNECT 2/src/components/images/medico2.jpg
Normal file
BIN
MEDICONNECT 2/src/components/images/medico2.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
MEDICONNECT 2/src/components/images/medico3.jpg
Normal file
BIN
MEDICONNECT 2/src/components/images/medico3.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
266
MEDICONNECT 2/src/pages/ResetPassword.tsx
Normal file
266
MEDICONNECT 2/src/pages/ResetPassword.tsx
Normal 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;
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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: [],
|
||||
|
||||
55
MEDICONNECT 2/test-password-recovery.js
Normal file
55
MEDICONNECT 2/test-password-recovery.js
Normal 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();
|
||||
54
MEDICONNECT 2/test-recovery-with-redirect.cjs
Normal file
54
MEDICONNECT 2/test-recovery-with-redirect.cjs
Normal 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);
|
||||
}
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user