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">
|
<html lang="pt-BR">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<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" />
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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 || "{}");
|
const body: LoginRequest = JSON.parse(event.body || "{}");
|
||||||
|
|
||||||
if (!body.email || !body.password) {
|
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 {
|
return {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
headers,
|
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 PerfilPaciente from "./pages/PerfilPaciente";
|
||||||
import ClearCache from "./pages/ClearCache";
|
import ClearCache from "./pages/ClearCache";
|
||||||
import AuthCallback from "./pages/AuthCallback";
|
import AuthCallback from "./pages/AuthCallback";
|
||||||
|
import ResetPassword from "./pages/ResetPassword";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -48,6 +49,7 @@ function App() {
|
|||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/clear-cache" element={<ClearCache />} />
|
<Route path="/clear-cache" element={<ClearCache />} />
|
||||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||||
|
<Route path="/reset-password" element={<ResetPassword />} />
|
||||||
<Route path="/paciente" element={<LoginPaciente />} />
|
<Route path="/paciente" element={<LoginPaciente />} />
|
||||||
<Route path="/login-secretaria" element={<LoginSecretaria />} />
|
<Route path="/login-secretaria" element={<LoginSecretaria />} />
|
||||||
<Route path="/login-medico" element={<LoginMedico />} />
|
<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 [searchTerm, setSearchTerm] = useState("");
|
||||||
const [statusFilter, setStatusFilter] = useState("Todos");
|
const [statusFilter, setStatusFilter] = useState("Todos");
|
||||||
const [typeFilter, setTypeFilter] = 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 () => {
|
const loadAppointments = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -68,8 +78,60 @@ export function SecretaryAppointmentList() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAppointments();
|
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 = () => {
|
const handleSearch = () => {
|
||||||
loadAppointments();
|
loadAppointments();
|
||||||
};
|
};
|
||||||
@ -137,7 +199,10 @@ export function SecretaryAppointmentList() {
|
|||||||
<h1 className="text-3xl font-bold text-gray-900">Consultas</h1>
|
<h1 className="text-3xl font-bold text-gray-900">Consultas</h1>
|
||||||
<p className="text-gray-600 mt-1">Gerencie as consultas agendadas</p>
|
<p className="text-gray-600 mt-1">Gerencie as consultas agendadas</p>
|
||||||
</div>
|
</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" />
|
<Plus className="h-4 w-4" />
|
||||||
Nova Consulta
|
Nova Consulta
|
||||||
</button>
|
</button>
|
||||||
@ -346,6 +411,116 @@ export function SecretaryAppointmentList() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -58,6 +58,7 @@ export function SecretaryDoctorSchedule() {
|
|||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
|
// Load availabilities
|
||||||
const availData = await availabilityService.list({
|
const availData = await availabilityService.list({
|
||||||
doctor_id: selectedDoctorId,
|
doctor_id: selectedDoctorId,
|
||||||
});
|
});
|
||||||
@ -68,7 +69,6 @@ export function SecretaryDoctorSchedule() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao carregar agenda:", error);
|
console.error("Erro ao carregar agenda:", error);
|
||||||
toast.error("Erro ao carregar agenda do médico");
|
toast.error("Erro ao carregar agenda do médico");
|
||||||
setAvailabilities([]);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -145,46 +145,14 @@ export function SecretaryDoctorSchedule() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!selectedDoctorId) {
|
|
||||||
toast.error("Selecione um médico");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log("📤 Criando disponibilidades para os dias:", selectedWeekdays);
|
// TODO: Implement availability creation
|
||||||
|
toast.success("Disponibilidade adicionada com sucesso");
|
||||||
// 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!`);
|
|
||||||
setShowAvailabilityDialog(false);
|
setShowAvailabilityDialog(false);
|
||||||
|
loadDoctorSchedule();
|
||||||
// Limpa o formulário
|
|
||||||
setSelectedWeekdays([]);
|
|
||||||
setStartTime("08:00");
|
|
||||||
setEndTime("18:00");
|
|
||||||
setDuration(30);
|
|
||||||
|
|
||||||
// Recarrega as disponibilidades
|
|
||||||
await loadDoctorSchedule();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("❌ Erro ao adicionar disponibilidade:", error);
|
console.error("Erro ao adicionar disponibilidade:", error);
|
||||||
toast.error("Erro ao adicionar disponibilidade. Verifique as permissões no banco de dados.");
|
toast.error("Erro ao adicionar disponibilidade");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -204,14 +172,15 @@ export function SecretaryDoctorSchedule() {
|
|||||||
toast.error("Erro ao adicionar exceção");
|
toast.error("Erro ao adicionar exceção");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const weekdays = [
|
const weekdays = [
|
||||||
{ value: "segunda", label: "Segunda" },
|
{ value: "monday", label: "Segunda" },
|
||||||
{ value: "terca", label: "Terça" },
|
{ value: "tuesday", label: "Terça" },
|
||||||
{ value: "quarta", label: "Quarta" },
|
{ value: "wednesday", label: "Quarta" },
|
||||||
{ value: "quinta", label: "Quinta" },
|
{ value: "thursday", label: "Quinta" },
|
||||||
{ value: "sexta", label: "Sexta" },
|
{ value: "friday", label: "Sexta" },
|
||||||
{ value: "sabado", label: "Sábado" },
|
{ value: "saturday", label: "Sábado" },
|
||||||
{ value: "domingo", label: "Domingo" },
|
{ value: "sunday", label: "Domingo" },
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -343,16 +312,16 @@ export function SecretaryDoctorSchedule() {
|
|||||||
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"
|
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900 capitalize">
|
<p className="font-medium text-gray-900">
|
||||||
{avail.weekday || "Não especificado"}
|
{avail.day_of_week}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-600">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<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">
|
<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>
|
</span>
|
||||||
<button
|
<button
|
||||||
title="Editar"
|
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 { useState, useEffect } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Search, Plus, Eye, Calendar, Edit, Trash2, X, RefreshCw } from "lucide-react";
|
import { Search, Plus, Eye, Calendar, Edit, Trash2, X } from "lucide-react";
|
||||||
import { patientService, type Patient } from "../../services";
|
import { patientService, userService, type Patient } from "../../services";
|
||||||
import PacienteForm, { type PacienteFormData } from "../pacientes/PacienteForm";
|
import PacienteForm, { type PacienteFormData } from "../pacientes/PacienteForm";
|
||||||
import { Avatar } from "../ui/Avatar";
|
import { Avatar } from "../ui/Avatar";
|
||||||
import { validarCPF } from "../../utils/validators";
|
|
||||||
|
|
||||||
const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"];
|
const BLOOD_TYPES = ["A+", "A-", "B+", "B-", "AB+", "AB-", "O+", "O-"];
|
||||||
|
|
||||||
@ -84,51 +83,14 @@ export function SecretaryPatientList() {
|
|||||||
const loadPatients = async () => {
|
const loadPatients = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
console.log("🔄 Carregando lista de pacientes...");
|
|
||||||
const data = await patientService.list();
|
const data = await patientService.list();
|
||||||
console.log("✅ Pacientes carregados:", {
|
console.log("✅ Pacientes carregados:", data);
|
||||||
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");
|
|
||||||
setPatients(Array.isArray(data) ? 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) {
|
if (Array.isArray(data) && data.length === 0) {
|
||||||
console.warn("⚠️ Nenhum paciente encontrado na API");
|
console.warn("⚠️ Nenhum paciente encontrado na API");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error("❌ Erro ao carregar pacientes:", {
|
console.error("❌ Erro ao carregar pacientes:", error);
|
||||||
message: error?.message,
|
|
||||||
response: error?.response?.data,
|
|
||||||
status: error?.response?.status
|
|
||||||
});
|
|
||||||
toast.error("Erro ao carregar pacientes");
|
toast.error("Erro ao carregar pacientes");
|
||||||
setPatients([]);
|
setPatients([]);
|
||||||
} finally {
|
} finally {
|
||||||
@ -273,120 +235,34 @@ export function SecretaryPatientList() {
|
|||||||
await patientService.update(formData.id, patientData);
|
await patientService.update(formData.id, patientData);
|
||||||
toast.success("Paciente atualizado com sucesso!");
|
toast.success("Paciente atualizado com sucesso!");
|
||||||
} else {
|
} else {
|
||||||
// Para criação, apenas cria o registro na tabela patients
|
// Para criação, usa o novo endpoint create-patient com validações completas
|
||||||
// O usuário de autenticação pode ser criado depois quando necessário
|
const createData = {
|
||||||
|
email: formData.email,
|
||||||
// Validação dos campos obrigatórios no frontend
|
full_name: formData.nome,
|
||||||
if (!formData.email || !formData.nome || !formData.cpf) {
|
cpf: formData.cpf,
|
||||||
toast.error("Por favor, preencha os campos obrigatórios: Email, Nome e CPF");
|
phone_mobile: formData.numeroTelefone,
|
||||||
return;
|
birth_date: formData.dataNascimento || undefined,
|
||||||
}
|
address: formData.endereco.rua
|
||||||
|
? `${formData.endereco.rua}${
|
||||||
// Remove formatação do CPF (deixa apenas números)
|
formData.endereco.numero ? ", " + formData.endereco.numero : ""
|
||||||
const cpfLimpo = formData.cpf.replace(/\D/g, "");
|
}${
|
||||||
|
formData.endereco.bairro ? " - " + formData.endereco.bairro : ""
|
||||||
if (cpfLimpo.length !== 11) {
|
}${
|
||||||
toast.error("CPF deve ter 11 dígitos");
|
formData.endereco.cidade ? " - " + formData.endereco.cidade : ""
|
||||||
return;
|
}${
|
||||||
}
|
formData.endereco.estado ? "/" + formData.endereco.estado : ""
|
||||||
|
}`
|
||||||
// Valida CPF
|
: undefined,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
await userService.createPatient(createData);
|
||||||
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);
|
|
||||||
|
|
||||||
toast.success("Paciente cadastrado com sucesso!");
|
toast.success("Paciente cadastrado com sucesso!");
|
||||||
}
|
}
|
||||||
|
|
||||||
setShowModal(false);
|
setShowModal(false);
|
||||||
|
loadPatients();
|
||||||
// Aguarda um pouco antes de recarregar para o banco propagar
|
} catch (error) {
|
||||||
console.log("⏳ Aguardando 1 segundo antes de recarregar a lista...");
|
console.error("Erro ao salvar paciente:", error);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
toast.error("Erro ao salvar paciente");
|
||||||
|
|
||||||
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);
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -405,14 +281,6 @@ export function SecretaryPatientList() {
|
|||||||
return colors[index % colors.length];
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -423,15 +291,6 @@ export function SecretaryPatientList() {
|
|||||||
Gerencie os pacientes cadastrados
|
Gerencie os pacientes cadastrados
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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
|
<button
|
||||||
onClick={handleNewPatient}
|
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"
|
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors"
|
||||||
@ -440,7 +299,6 @@ export function SecretaryPatientList() {
|
|||||||
Novo Paciente
|
Novo Paciente
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search and Filters */}
|
{/* Search and Filters */}
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6 space-y-4">
|
||||||
@ -607,8 +465,7 @@ export function SecretaryPatientList() {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))
|
))
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { Search, FileText, Download } from "lucide-react";
|
import { Search, FileText, Download, Plus } from "lucide-react";
|
||||||
import { reportService, type Report } from "../../services";
|
import {
|
||||||
|
reportService,
|
||||||
|
type Report,
|
||||||
|
patientService,
|
||||||
|
type Patient,
|
||||||
|
} from "../../services";
|
||||||
|
|
||||||
export function SecretaryReportList() {
|
export function SecretaryReportList() {
|
||||||
const [reports, setReports] = useState<Report[]>([]);
|
const [reports, setReports] = useState<Report[]>([]);
|
||||||
@ -9,11 +14,64 @@ export function SecretaryReportList() {
|
|||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [typeFilter, setTypeFilter] = useState("Todos");
|
const [typeFilter, setTypeFilter] = useState("Todos");
|
||||||
const [periodFilter, setPeriodFilter] = 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(() => {
|
useEffect(() => {
|
||||||
loadReports();
|
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 () => {
|
const loadReports = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
@ -68,9 +126,12 @@ export function SecretaryReportList() {
|
|||||||
Visualize e baixe relatórios do sistema
|
Visualize e baixe relatórios do sistema
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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
|
||||||
<FileText className="h-4 w-4" />
|
onClick={handleOpenCreateModal}
|
||||||
Gerar Relatório
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -242,6 +303,103 @@ export function SecretaryReportList() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
Headphones,
|
Headphones,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import Chatbot from "../components/Chatbot";
|
||||||
|
|
||||||
interface FAQ {
|
interface FAQ {
|
||||||
question: string;
|
question: string;
|
||||||
@ -404,6 +405,9 @@ const CentralAjuda: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Chatbot Widget */}
|
||||||
|
<Chatbot />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import {
|
|||||||
Headphones,
|
Headphones,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import Chatbot from "../components/Chatbot";
|
||||||
|
|
||||||
interface FAQ {
|
interface FAQ {
|
||||||
question: string;
|
question: string;
|
||||||
@ -408,6 +409,9 @@ const CentralAjudaMedico: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Chatbot Widget */}
|
||||||
|
<Chatbot />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Calendar, Users, UserCheck, Clock, ArrowRight } from "lucide-react";
|
|||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { patientService, doctorService, appointmentService } from "../services";
|
import { patientService, doctorService, appointmentService } from "../services";
|
||||||
import { MetricCard } from "../components/MetricCard";
|
import { MetricCard } from "../components/MetricCard";
|
||||||
|
import { HeroBanner } from "../components/HeroBanner";
|
||||||
import { i18n } from "../i18n";
|
import { i18n } from "../i18n";
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
|
|
||||||
@ -96,64 +97,8 @@ const Home: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8" id="main-content">
|
<div className="space-y-8" id="main-content">
|
||||||
{/* Hero Section */}
|
{/* Hero Section com Background Rotativo */}
|
||||||
<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">
|
<HeroBanner />
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
{/* Métricas */}
|
{/* Métricas */}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -3,6 +3,16 @@ import AvatarInitials from "../components/AvatarInitials";
|
|||||||
import { Stethoscope, Mail, Phone, AlertTriangle } from "lucide-react";
|
import { Stethoscope, Mail, Phone, AlertTriangle } from "lucide-react";
|
||||||
import { doctorService } from "../services";
|
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 ListaMedicos: React.FC = () => {
|
||||||
const [medicos, setMedicos] = useState<MedicoDetalhado[]>([]);
|
const [medicos, setMedicos] = useState<MedicoDetalhado[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
@ -14,21 +24,25 @@ const ListaMedicos: React.FC = () => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const resp = await doctorService.listarMedicos({ status: "ativo" });
|
const list = await doctorService.list({ active: true });
|
||||||
if (!resp.success) {
|
|
||||||
if (!cancelled) {
|
|
||||||
setError(resp.error || "Falha ao carregar médicos");
|
|
||||||
setMedicos([]);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const list = resp.data?.data || [];
|
|
||||||
if (!list.length) {
|
if (!list.length) {
|
||||||
console.warn(
|
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.'
|
'[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) {
|
} catch (e) {
|
||||||
console.error("Erro inesperado ao listar médicos", e);
|
console.error("Erro inesperado ao listar médicos", e);
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Mail, Lock, Stethoscope } from "lucide-react";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
import { authService } from "../services";
|
import { authService, userService } from "../services";
|
||||||
|
|
||||||
const LoginMedico: React.FC = () => {
|
const LoginMedico: React.FC = () => {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@ -22,21 +22,61 @@ const LoginMedico: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
console.log("[LoginMedico] Fazendo login com email:", formData.email);
|
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);
|
const ok = await loginComEmailSenha(formData.email, formData.senha);
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
console.log(
|
console.log(
|
||||||
"[LoginMedico] Login bem-sucedido! Navegando para /painel-medico"
|
"[LoginMedico] Login bem-sucedido! Navegando para /painel-medico"
|
||||||
);
|
);
|
||||||
toast.success("Login realizado com sucesso!");
|
toast.success(`Bem-vindo, ${userName}!`);
|
||||||
navigate("/painel-medico");
|
navigate("/painel-medico");
|
||||||
} else {
|
} else {
|
||||||
console.error("[LoginMedico] loginComEmailSenha retornou false");
|
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);
|
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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -117,16 +157,15 @@ const LoginMedico: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await authService.sendMagicLink(
|
await authService.requestPasswordReset(formData.email);
|
||||||
formData.email,
|
toast.success(
|
||||||
"https://mediconnectbrasil.netlify.app/medico/painel"
|
"Email de recuperação enviado! Verifique sua caixa de entrada."
|
||||||
);
|
);
|
||||||
toast.success("Link de acesso enviado para seu email!");
|
|
||||||
} catch {
|
} 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?
|
Esqueceu a senha?
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { User, Mail, Lock } from "lucide-react";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
import { authService, patientService } from "../services";
|
import { authService, patientService, userService } from "../services";
|
||||||
|
|
||||||
const LoginPaciente: React.FC = () => {
|
const LoginPaciente: React.FC = () => {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@ -32,47 +32,65 @@ const LoginPaciente: React.FC = () => {
|
|||||||
console.log("[LoginPaciente] Fazendo login com email:", formData.email);
|
console.log("[LoginPaciente] Fazendo login com email:", formData.email);
|
||||||
|
|
||||||
// Fazer login via API Supabase
|
// Fazer login via API Supabase
|
||||||
await authService.login({
|
const loginResponse = await authService.login({
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
password: formData.senha,
|
password: formData.senha,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("[LoginPaciente] Login bem-sucedido!");
|
console.log("[LoginPaciente] Login bem-sucedido!", loginResponse);
|
||||||
|
|
||||||
// Buscar dados do paciente da API
|
// Buscar informações completas do usuário (profile + roles)
|
||||||
const pacientes = await patientService.list();
|
let userName = loginResponse.user.email?.split("@")[0] || "Paciente";
|
||||||
const paciente = pacientes.find((p: any) => p.email === formData.email);
|
|
||||||
|
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);
|
console.log("[LoginPaciente] Paciente encontrado:", paciente);
|
||||||
|
|
||||||
if (paciente) {
|
// Usar nome do paciente se disponível, senão usar do profile
|
||||||
console.log("[LoginPaciente] Paciente encontrado:", {
|
const finalName = paciente?.full_name || userName;
|
||||||
id: paciente.id,
|
|
||||||
nome: paciente.full_name,
|
|
||||||
email: paciente.email,
|
|
||||||
});
|
|
||||||
const ok = await loginPaciente({
|
const ok = await loginPaciente({
|
||||||
id: paciente.id,
|
id: paciente?.id || loginResponse.user.id,
|
||||||
nome: paciente.full_name,
|
nome: finalName,
|
||||||
email: paciente.email,
|
email: loginResponse.user.email || formData.email,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
console.log("[LoginPaciente] Navegando para /acompanhamento");
|
console.log("[LoginPaciente] Navegando para /acompanhamento");
|
||||||
|
toast.success(`Bem-vindo, ${finalName}!`);
|
||||||
navigate("/acompanhamento");
|
navigate("/acompanhamento");
|
||||||
} else {
|
} else {
|
||||||
console.error("[LoginPaciente] loginPaciente retornou false");
|
console.error("[LoginPaciente] loginPaciente retornou false");
|
||||||
toast.error("Erro ao processar login");
|
toast.error("Erro ao processar login");
|
||||||
}
|
}
|
||||||
} else {
|
} catch (error: unknown) {
|
||||||
console.log("[LoginPaciente] Paciente não encontrado na lista");
|
|
||||||
toast.error(
|
|
||||||
"Dados do paciente não encontrados. Entre em contato com o suporte."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[LoginPaciente] Erro no login:", error);
|
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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -141,16 +159,20 @@ const LoginPaciente: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
setShowCadastro(false);
|
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:", {
|
console.error("[LoginPaciente] Erro ao cadastrar:", {
|
||||||
error,
|
error,
|
||||||
response: error?.response,
|
response: err?.response,
|
||||||
data: error?.response?.data,
|
data: err?.response?.data,
|
||||||
});
|
});
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error?.response?.data?.error ||
|
err?.response?.data?.error ||
|
||||||
error?.response?.data?.message ||
|
err?.response?.data?.message ||
|
||||||
error?.message ||
|
err?.message ||
|
||||||
"Erro ao realizar cadastro";
|
"Erro ao realizar cadastro";
|
||||||
toast.error(errorMessage);
|
toast.error(errorMessage);
|
||||||
} finally {
|
} 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 (
|
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="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">
|
<div className="max-w-md w-full">
|
||||||
@ -307,38 +267,25 @@ const LoginPaciente: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await authService.sendMagicLink(
|
await authService.requestPasswordReset(formData.email);
|
||||||
formData.email,
|
toast.success(
|
||||||
"https://mediconnectbrasil.netlify.app/paciente/agendamento"
|
"Email de recuperação enviado! Verifique sua caixa de entrada."
|
||||||
);
|
);
|
||||||
toast.success("Link de acesso enviado para seu email!");
|
|
||||||
} catch {
|
} 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?
|
Esqueceu a senha?
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/** Botão original (remoto) comentado a pedido **/}
|
|
||||||
{/**
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
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"
|
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>
|
|
||||||
**/}
|
|
||||||
|
|
||||||
<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"
|
|
||||||
>
|
>
|
||||||
{loading ? "Entrando..." : "Entrar"}
|
{loading ? "Entrando..." : "Entrar"}
|
||||||
</button>
|
</button>
|
||||||
@ -373,8 +320,9 @@ const LoginPaciente: React.FC = () => {
|
|||||||
"Link de acesso enviado para seu email! Verifique sua caixa de entrada.",
|
"Link de acesso enviado para seu email! Verifique sua caixa de entrada.",
|
||||||
{ duration: 6000 }
|
{ duration: 6000 }
|
||||||
);
|
);
|
||||||
} catch (error: any) {
|
} catch (error: unknown) {
|
||||||
toast.error(error?.message || "Erro ao enviar link");
|
const err = error as { message?: string };
|
||||||
|
toast.error(err?.message || "Erro ao enviar link");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { Mail, Lock, Clipboard } from "lucide-react";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
import { authService } from "../services";
|
import { authService, userService } from "../services";
|
||||||
|
|
||||||
const LoginSecretaria: React.FC = () => {
|
const LoginSecretaria: React.FC = () => {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
@ -22,21 +22,73 @@ const LoginSecretaria: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
console.log("[LoginSecretaria] Fazendo login com email:", formData.email);
|
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);
|
const ok = await loginComEmailSenha(formData.email, formData.senha);
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
console.log(
|
console.log(
|
||||||
"[LoginSecretaria] Login bem-sucedido! Navegando para /painel-secretaria"
|
"[LoginSecretaria] Login bem-sucedido! Navegando para /painel-secretaria"
|
||||||
);
|
);
|
||||||
toast.success("Login realizado com sucesso!");
|
toast.success(`Bem-vinda, ${userName}!`);
|
||||||
navigate("/painel-secretaria");
|
navigate("/painel-secretaria");
|
||||||
} else {
|
} else {
|
||||||
console.error("[LoginSecretaria] loginComEmailSenha retornou false");
|
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);
|
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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@ -117,13 +169,10 @@ const LoginSecretaria: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await authService.sendMagicLink(
|
await authService.requestPasswordReset(formData.email);
|
||||||
formData.email,
|
toast.success("Email de recuperação enviado!");
|
||||||
"https://mediconnectbrasil.netlify.app/secretaria/painel"
|
|
||||||
);
|
|
||||||
toast.success("Link de acesso enviado para seu email!");
|
|
||||||
} catch {
|
} 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"
|
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 { useState, useEffect } from "react";
|
||||||
import { Save } from "lucide-react";
|
import { Save, ArrowLeft } from "lucide-react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useAuth } from "../hooks/useAuth";
|
import { useAuth } from "../hooks/useAuth";
|
||||||
import { patientService } from "../services";
|
import { patientService } from "../services";
|
||||||
import { AvatarUpload } from "../components/ui/AvatarUpload";
|
import { AvatarUpload } from "../components/ui/AvatarUpload";
|
||||||
|
|
||||||
export default function PerfilPaciente() {
|
export default function PerfilPaciente() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [activeTab, setActiveTab] = useState<
|
const [activeTab, setActiveTab] = useState<
|
||||||
@ -47,13 +49,20 @@ export default function PerfilPaciente() {
|
|||||||
}, [user?.id]);
|
}, [user?.id]);
|
||||||
|
|
||||||
const loadPatientData = async () => {
|
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 {
|
try {
|
||||||
setLoading(true);
|
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({
|
setFormData({
|
||||||
full_name: patient.full_name || "",
|
full_name: patient.full_name || "",
|
||||||
email: patient.email || "",
|
email: patient.email || "",
|
||||||
@ -72,11 +81,37 @@ export default function PerfilPaciente() {
|
|||||||
weight_kg: patient.weight_kg?.toString() || "",
|
weight_kg: patient.weight_kg?.toString() || "",
|
||||||
height_m: patient.height_m?.toString() || "",
|
height_m: patient.height_m?.toString() || "",
|
||||||
});
|
});
|
||||||
// Patient type não tem avatar_url ainda
|
|
||||||
setAvatarUrl(undefined);
|
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) {
|
} 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");
|
toast.error("Erro ao carregar dados do perfil");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@ -94,11 +129,31 @@ export default function PerfilPaciente() {
|
|||||||
: undefined,
|
: undefined,
|
||||||
height_m: formData.height_m ? parseFloat(formData.height_m) : undefined,
|
height_m: formData.height_m ? parseFloat(formData.height_m) : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Tentar atualizar primeiro
|
||||||
await patientService.update(user.id, dataToSave);
|
await patientService.update(user.id, dataToSave);
|
||||||
toast.success("Perfil atualizado com sucesso!");
|
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);
|
setIsEditing(false);
|
||||||
|
// Recarregar dados
|
||||||
|
await loadPatientData();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao salvar perfil:", error);
|
console.error("[PerfilPaciente] Erro ao salvar perfil:", error);
|
||||||
toast.error("Erro ao salvar perfil");
|
toast.error("Erro ao salvar perfil");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -134,20 +189,54 @@ export default function PerfilPaciente() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||||
<div className="w-16 h-16 border-4 border-blue-600 border-t-transparent rounded-full animate-spin" />
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<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 */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-bold text-gray-900">Meu Perfil</h1>
|
<h1 className="text-3xl font-bold text-gray-900 dark:text-gray-100">
|
||||||
<p className="text-gray-600">
|
Meu Perfil
|
||||||
|
</h1>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400">
|
||||||
Gerencie suas informações pessoais e médicas
|
Gerencie suas informações pessoais e médicas
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -165,7 +254,7 @@ export default function PerfilPaciente() {
|
|||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
loadPatientData();
|
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
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
@ -181,8 +270,10 @@ export default function PerfilPaciente() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Avatar Card */}
|
{/* Avatar Card */}
|
||||||
<div className="bg-white rounded-lg shadow p-6">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
|
||||||
<h2 className="text-lg font-semibold mb-4">Foto de Perfil</h2>
|
<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">
|
<div className="flex items-center gap-6">
|
||||||
<AvatarUpload
|
<AvatarUpload
|
||||||
userId={user?.id}
|
userId={user?.id}
|
||||||
@ -194,22 +285,26 @@ export default function PerfilPaciente() {
|
|||||||
onAvatarUpdate={(url) => setAvatarUrl(url || undefined)}
|
onAvatarUpdate={(url) => setAvatarUrl(url || undefined)}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">{formData.full_name}</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">
|
||||||
<p className="text-gray-500">{formData.email}</p>
|
{formData.full_name || "Carregando..."}
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400">
|
||||||
|
{formData.email || "Sem email"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabs */}
|
{/* Tabs */}
|
||||||
<div className="bg-white rounded-lg shadow">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow">
|
||||||
<div className="border-b border-gray-200">
|
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||||
<nav className="flex -mb-px">
|
<nav className="flex -mb-px">
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("personal")}
|
onClick={() => setActiveTab("personal")}
|
||||||
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
|
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||||
activeTab === "personal"
|
activeTab === "personal"
|
||||||
? "border-blue-600 text-blue-600"
|
? "border-blue-600 text-blue-600 dark:text-blue-400"
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
: "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
|
Dados Pessoais
|
||||||
@ -218,8 +313,8 @@ export default function PerfilPaciente() {
|
|||||||
onClick={() => setActiveTab("medical")}
|
onClick={() => setActiveTab("medical")}
|
||||||
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
|
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||||
activeTab === "medical"
|
activeTab === "medical"
|
||||||
? "border-blue-600 text-blue-600"
|
? "border-blue-600 text-blue-600 dark:text-blue-400"
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
: "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
|
Informações Médicas
|
||||||
@ -228,8 +323,8 @@ export default function PerfilPaciente() {
|
|||||||
onClick={() => setActiveTab("security")}
|
onClick={() => setActiveTab("security")}
|
||||||
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
|
className={`px-6 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||||
activeTab === "security"
|
activeTab === "security"
|
||||||
? "border-blue-600 text-blue-600"
|
? "border-blue-600 text-blue-600 dark:text-blue-400"
|
||||||
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
|
: "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
|
Segurança
|
||||||
@ -242,16 +337,16 @@ export default function PerfilPaciente() {
|
|||||||
{activeTab === "personal" && (
|
{activeTab === "personal" && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<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
|
Informações Pessoais
|
||||||
</h3>
|
</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
|
Mantenha seus dados atualizados
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<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">
|
||||||
Nome Completo
|
Nome Completo
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -261,12 +356,12 @@ export default function PerfilPaciente() {
|
|||||||
handleChange("full_name", e.target.value)
|
handleChange("full_name", e.target.value)
|
||||||
}
|
}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
|
|
||||||
<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
|
Email
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -274,12 +369,12 @@ export default function PerfilPaciente() {
|
|||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => handleChange("email", e.target.value)}
|
onChange={(e) => handleChange("email", e.target.value)}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
|
|
||||||
<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
|
Telefone
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -289,12 +384,12 @@ export default function PerfilPaciente() {
|
|||||||
handleChange("phone_mobile", e.target.value)
|
handleChange("phone_mobile", e.target.value)
|
||||||
}
|
}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
|
|
||||||
<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
|
CPF
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -306,7 +401,7 @@ export default function PerfilPaciente() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
Data de Nascimento
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -316,19 +411,19 @@ export default function PerfilPaciente() {
|
|||||||
handleChange("birth_date", e.target.value)
|
handleChange("birth_date", e.target.value)
|
||||||
}
|
}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
|
|
||||||
<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
|
Sexo
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={formData.sex}
|
value={formData.sex}
|
||||||
onChange={(e) => handleChange("sex", e.target.value)}
|
onChange={(e) => handleChange("sex", e.target.value)}
|
||||||
disabled={!isEditing}
|
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="">Selecione</option>
|
||||||
<option value="M">Masculino</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="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div className="md:col-span-2">
|
<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
|
Rua
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -352,12 +447,12 @@ export default function PerfilPaciente() {
|
|||||||
value={formData.street}
|
value={formData.street}
|
||||||
onChange={(e) => handleChange("street", e.target.value)}
|
onChange={(e) => handleChange("street", e.target.value)}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
|
|
||||||
<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
|
Número
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -365,12 +460,12 @@ export default function PerfilPaciente() {
|
|||||||
value={formData.number}
|
value={formData.number}
|
||||||
onChange={(e) => handleChange("number", e.target.value)}
|
onChange={(e) => handleChange("number", e.target.value)}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
|
|
||||||
<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
|
Complemento
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -380,12 +475,12 @@ export default function PerfilPaciente() {
|
|||||||
handleChange("complement", e.target.value)
|
handleChange("complement", e.target.value)
|
||||||
}
|
}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
|
|
||||||
<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
|
Bairro
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -395,12 +490,12 @@ export default function PerfilPaciente() {
|
|||||||
handleChange("neighborhood", e.target.value)
|
handleChange("neighborhood", e.target.value)
|
||||||
}
|
}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
|
|
||||||
<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
|
Cidade
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -408,12 +503,12 @@ export default function PerfilPaciente() {
|
|||||||
value={formData.city}
|
value={formData.city}
|
||||||
onChange={(e) => handleChange("city", e.target.value)}
|
onChange={(e) => handleChange("city", e.target.value)}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
|
|
||||||
<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
|
Estado
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -422,12 +517,12 @@ export default function PerfilPaciente() {
|
|||||||
onChange={(e) => handleChange("state", e.target.value)}
|
onChange={(e) => handleChange("state", e.target.value)}
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
maxLength={2}
|
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>
|
||||||
|
|
||||||
<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
|
CEP
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -435,7 +530,7 @@ export default function PerfilPaciente() {
|
|||||||
value={formData.cep}
|
value={formData.cep}
|
||||||
onChange={(e) => handleChange("cep", e.target.value)}
|
onChange={(e) => handleChange("cep", e.target.value)}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
</div>
|
</div>
|
||||||
@ -456,7 +551,7 @@ export default function PerfilPaciente() {
|
|||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<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">
|
||||||
Tipo Sanguíneo
|
Tipo Sanguíneo
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
@ -465,7 +560,7 @@ export default function PerfilPaciente() {
|
|||||||
handleChange("blood_type", e.target.value)
|
handleChange("blood_type", e.target.value)
|
||||||
}
|
}
|
||||||
disabled={!isEditing}
|
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="">Selecione</option>
|
||||||
<option value="A+">A+</option>
|
<option value="A+">A+</option>
|
||||||
@ -479,7 +574,7 @@ export default function PerfilPaciente() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<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)
|
Peso (kg)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -490,12 +585,12 @@ export default function PerfilPaciente() {
|
|||||||
handleChange("weight_kg", e.target.value)
|
handleChange("weight_kg", e.target.value)
|
||||||
}
|
}
|
||||||
disabled={!isEditing}
|
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>
|
||||||
|
|
||||||
<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)
|
Altura (m)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -507,7 +602,7 @@ export default function PerfilPaciente() {
|
|||||||
}
|
}
|
||||||
disabled={!isEditing}
|
disabled={!isEditing}
|
||||||
placeholder="Ex: 1.75"
|
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>
|
||||||
</div>
|
</div>
|
||||||
@ -526,7 +621,7 @@ export default function PerfilPaciente() {
|
|||||||
|
|
||||||
<div className="max-w-md space-y-4">
|
<div className="max-w-md space-y-4">
|
||||||
<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">
|
||||||
Senha Atual
|
Senha Atual
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -544,7 +639,7 @@ export default function PerfilPaciente() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
Nova Senha
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@ -562,7 +657,7 @@ export default function PerfilPaciente() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
Confirmar Nova Senha
|
||||||
</label>
|
</label>
|
||||||
<input
|
<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
|
* Cliente HTTP usando Axios
|
||||||
* Todas as requisições passam pelas Netlify Functions
|
* Chamadas diretas ao Supabase
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
|
||||||
@ -11,10 +11,11 @@ class ApiClient {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.client = axios.create({
|
this.client = axios.create({
|
||||||
baseURL: API_CONFIG.BASE_URL,
|
baseURL: API_CONFIG.REST_URL,
|
||||||
timeout: API_CONFIG.TIMEOUT,
|
timeout: API_CONFIG.TIMEOUT,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
apikey: API_CONFIG.SUPABASE_ANON_KEY,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -24,10 +25,10 @@ class ApiClient {
|
|||||||
private setupInterceptors() {
|
private setupInterceptors() {
|
||||||
// Request interceptor - adiciona token automaticamente
|
// Request interceptor - adiciona token automaticamente
|
||||||
this.client.interceptors.request.use(
|
this.client.interceptors.request.use(
|
||||||
(config: any) => {
|
(config) => {
|
||||||
// Não adicionar token se a flag _skipAuth estiver presente
|
// Não adicionar token se a flag _skipAuth estiver presente
|
||||||
if (config._skipAuth) {
|
if ((config as any)._skipAuth) {
|
||||||
delete config._skipAuth;
|
delete (config as any)._skipAuth;
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,9 +89,20 @@ class ApiClient {
|
|||||||
|
|
||||||
if (refreshToken) {
|
if (refreshToken) {
|
||||||
console.log("[ApiClient] Refresh token encontrado, renovando...");
|
console.log("[ApiClient] Refresh token encontrado, renovando...");
|
||||||
const response = await this.client.post("/auth-refresh", {
|
|
||||||
|
// Chama Supabase diretamente para renovar token
|
||||||
|
const response = await axios.post(
|
||||||
|
`${API_CONFIG.AUTH_URL}/token?grant_type=refresh_token`,
|
||||||
|
{
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
});
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
apikey: API_CONFIG.SUPABASE_ANON_KEY,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
access_token,
|
access_token,
|
||||||
|
|||||||
@ -1,18 +1,25 @@
|
|||||||
/**
|
/**
|
||||||
* Configuração da API
|
* 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
|
export const SUPABASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
|
||||||
// Em produção, usa URL completa do Netlify
|
export const SUPABASE_ANON_KEY =
|
||||||
const isDevelopment = import.meta.env.DEV;
|
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
|
||||||
const BASE_URL = isDevelopment
|
|
||||||
? "http://localhost:8888/.netlify/functions"
|
// URL base do app
|
||||||
: "https://mediconnectbrasil.netlify.app/.netlify/functions";
|
export const APP_URL = "https://mediconnectbrasil.app";
|
||||||
|
|
||||||
export const API_CONFIG = {
|
export const API_CONFIG = {
|
||||||
// Base URL aponta para suas Netlify Functions
|
// Base URLs do Supabase
|
||||||
BASE_URL,
|
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 padrão (30 segundos)
|
||||||
TIMEOUT: 30000,
|
TIMEOUT: 30000,
|
||||||
|
|||||||
@ -72,8 +72,13 @@ class AppointmentService {
|
|||||||
* Busca agendamento por ID
|
* Busca agendamento por ID
|
||||||
*/
|
*/
|
||||||
async getById(id: string): Promise<Appointment> {
|
async getById(id: string): Promise<Appointment> {
|
||||||
const response = await apiClient.get<Appointment>(`${this.basePath}/${id}`);
|
const response = await apiClient.get<Appointment[]>(
|
||||||
return response.data;
|
`${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
|
* Atualiza agendamento existente
|
||||||
*/
|
*/
|
||||||
async update(id: string, data: UpdateAppointmentInput): Promise<Appointment> {
|
async update(id: string, data: UpdateAppointmentInput): Promise<Appointment> {
|
||||||
const response = await apiClient.patch<Appointment>(
|
const response = await apiClient.patch<Appointment[]>(
|
||||||
`${this.basePath}/${id}`,
|
`${this.basePath}?id=eq.${id}`,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
return response.data;
|
if (response.data && response.data.length > 0) {
|
||||||
|
return response.data[0];
|
||||||
|
}
|
||||||
|
throw new Error("Agendamento não encontrado");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deleta agendamento
|
* Deleta agendamento
|
||||||
*/
|
*/
|
||||||
async delete(id: string): Promise<void> {
|
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)
|
* 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 { API_CONFIG } from "../api/config";
|
||||||
import type {
|
import type {
|
||||||
LoginInput,
|
LoginInput,
|
||||||
@ -18,9 +18,78 @@ class AuthService {
|
|||||||
*/
|
*/
|
||||||
async login(credentials: LoginInput): Promise<LoginResponse> {
|
async login(credentials: LoginInput): Promise<LoginResponse> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post<LoginResponse>(
|
console.log("[authService] Tentando login com:", credentials.email);
|
||||||
"/auth-login",
|
const response = await axios.post<LoginResponse>(
|
||||||
credentials
|
`${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
|
// Salva tokens e user no localStorage
|
||||||
@ -41,7 +110,7 @@ class AuthService {
|
|||||||
|
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro no login:", error);
|
console.error("Erro no signup:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,14 +124,27 @@ class AuthService {
|
|||||||
redirectUrl?: string
|
redirectUrl?: string
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post<{
|
await axios.post(
|
||||||
success: boolean;
|
`${API_CONFIG.AUTH_URL}/otp`,
|
||||||
message: string;
|
{
|
||||||
}>("/auth-magic-link", {
|
|
||||||
email,
|
email,
|
||||||
redirect_url: redirectUrl,
|
options: {
|
||||||
});
|
emailRedirectTo:
|
||||||
return response.data;
|
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) {
|
} catch (error) {
|
||||||
console.error("Erro ao enviar magic link:", error);
|
console.error("Erro ao enviar magic link:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -71,34 +153,94 @@ class AuthService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Solicita reset de senha via email (público)
|
* Solicita reset de senha via email (público)
|
||||||
* POST /request-password-reset
|
* POST /auth/v1/recover
|
||||||
*/
|
*/
|
||||||
async requestPasswordReset(
|
async requestPasswordReset(
|
||||||
email: string,
|
email: string,
|
||||||
redirectUrl?: string
|
redirectUrl?: string
|
||||||
): Promise<{ success: boolean; message: string }> {
|
): Promise<{ success: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
const response = await (apiClient as any).postPublic<{
|
await axios.post(
|
||||||
success: boolean;
|
`${API_CONFIG.AUTH_URL}/recover`,
|
||||||
message: string;
|
{
|
||||||
}>("/request-password-reset", {
|
|
||||||
email,
|
email,
|
||||||
redirect_url: redirectUrl,
|
options: {
|
||||||
});
|
redirectTo: redirectUrl || `${API_CONFIG.APP_URL}/reset-password`,
|
||||||
return response.data;
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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) {
|
} catch (error) {
|
||||||
console.error("Erro ao solicitar reset de senha:", error);
|
console.error("Erro ao solicitar reset de senha:", error);
|
||||||
throw 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)
|
* Faz logout (invalida sessão no servidor e limpa localStorage)
|
||||||
*/
|
*/
|
||||||
async logout(): Promise<void> {
|
async logout(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
|
||||||
|
|
||||||
|
if (token) {
|
||||||
// Chama API para invalidar sessão no servidor
|
// Chama API para invalidar sessão no servidor
|
||||||
await apiClient.post("/auth-logout");
|
await axios.post(
|
||||||
|
`${API_CONFIG.AUTH_URL}/logout`,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
apikey: API_CONFIG.SUPABASE_ANON_KEY,
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao invalidar sessão no servidor:", error);
|
console.error("Erro ao invalidar sessão no servidor:", error);
|
||||||
// Continua mesmo com erro, para garantir limpeza local
|
// Continua mesmo com erro, para garantir limpeza local
|
||||||
@ -151,10 +293,16 @@ class AuthService {
|
|||||||
throw new Error("Refresh token não encontrado");
|
throw new Error("Refresh token não encontrado");
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiClient.post<RefreshTokenResponse>(
|
const response = await axios.post<RefreshTokenResponse>(
|
||||||
"/auth-refresh",
|
`${API_CONFIG.AUTH_URL}/token?grant_type=refresh_token`,
|
||||||
{
|
{
|
||||||
refresh_token: refreshToken,
|
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
|
* Cria uma nova configuração de disponibilidade
|
||||||
*/
|
*/
|
||||||
async create(data: CreateAvailabilityInput): Promise<DoctorAvailability> {
|
async create(data: CreateAvailabilityInput): Promise<DoctorAvailability> {
|
||||||
console.log("[availabilityService.create] 📤 Enviando dados:", JSON.stringify(data, null, 2));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await apiClient.post<DoctorAvailability>(
|
const response = await apiClient.post<DoctorAvailability>(
|
||||||
this.basePath,
|
this.basePath,
|
||||||
data
|
data
|
||||||
);
|
);
|
||||||
console.log("[availabilityService.create] ✅ Resposta:", response.data);
|
|
||||||
return 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -53,8 +53,11 @@ class DoctorService {
|
|||||||
*/
|
*/
|
||||||
async getById(id: string): Promise<Doctor> {
|
async getById(id: string): Promise<Doctor> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get<Doctor>(`/doctors/${id}`);
|
const response = await apiClient.get<Doctor[]>(`/doctors?id=eq.${id}`);
|
||||||
return response.data;
|
if (response.data && response.data.length > 0) {
|
||||||
|
return response.data[0];
|
||||||
|
}
|
||||||
|
throw new Error("Médico não encontrado");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao buscar médico:", error);
|
console.error("Erro ao buscar médico:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -79,8 +82,14 @@ class DoctorService {
|
|||||||
*/
|
*/
|
||||||
async update(id: string, data: UpdateDoctorInput): Promise<Doctor> {
|
async update(id: string, data: UpdateDoctorInput): Promise<Doctor> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.patch<Doctor>(`/doctors/${id}`, data);
|
const response = await apiClient.patch<Doctor[]>(
|
||||||
return response.data;
|
`/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) {
|
} catch (error) {
|
||||||
console.error("Erro ao atualizar médico:", error);
|
console.error("Erro ao atualizar médico:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -92,7 +101,7 @@ class DoctorService {
|
|||||||
*/
|
*/
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(`/doctors/${id}`);
|
await apiClient.delete(`/doctors?id=eq.${id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao deletar médico:", error);
|
console.error("Erro ao deletar médico:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@ -36,22 +36,12 @@ class PatientService {
|
|||||||
const queryString = params.toString();
|
const queryString = params.toString();
|
||||||
const url = queryString ? `/patients?${queryString}` : "/patients";
|
const url = queryString ? `/patients?${queryString}` : "/patients";
|
||||||
|
|
||||||
console.log(`[patientService.list] 📤 Chamando: ${url}`);
|
|
||||||
const response = await apiClient.get<Patient[]>(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;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Silenciar erro 401 (não autenticado) - é esperado em páginas públicas
|
// Silenciar erro 401 (não autenticado) - é esperado em páginas públicas
|
||||||
if (error?.response?.status !== 401) {
|
if (error?.response?.status !== 401) {
|
||||||
console.error("[patientService.list] ❌ Erro ao listar pacientes:", {
|
console.error("Erro ao listar pacientes:", error);
|
||||||
message: error?.message,
|
|
||||||
response: error?.response?.data,
|
|
||||||
status: error?.response?.status
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
@ -62,10 +52,22 @@ class PatientService {
|
|||||||
*/
|
*/
|
||||||
async getById(id: string): Promise<Patient> {
|
async getById(id: string): Promise<Patient> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get<Patient>(`/patients/${id}`);
|
console.log("[patientService] Buscando paciente por ID:", id);
|
||||||
return response.data;
|
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) {
|
} catch (error) {
|
||||||
console.error("Erro ao buscar paciente:", error);
|
console.error("[patientService] Erro ao buscar paciente:", error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,8 +90,14 @@ class PatientService {
|
|||||||
*/
|
*/
|
||||||
async update(id: string, data: UpdatePatientInput): Promise<Patient> {
|
async update(id: string, data: UpdatePatientInput): Promise<Patient> {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.patch<Patient>(`/patients/${id}`, data);
|
const response = await apiClient.patch<Patient[]>(
|
||||||
return response.data;
|
`/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) {
|
} catch (error) {
|
||||||
console.error("Erro ao atualizar paciente:", error);
|
console.error("Erro ao atualizar paciente:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -101,7 +109,7 @@ class PatientService {
|
|||||||
*/
|
*/
|
||||||
async delete(id: string): Promise<void> {
|
async delete(id: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await apiClient.delete(`/patients/${id}`);
|
await apiClient.delete(`/patients?id=eq.${id}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao deletar paciente:", error);
|
console.error("Erro ao deletar paciente:", error);
|
||||||
throw error;
|
throw error;
|
||||||
@ -114,27 +122,22 @@ class PatientService {
|
|||||||
*/
|
*/
|
||||||
async register(data: RegisterPatientInput): Promise<RegisterPatientResponse> {
|
async register(data: RegisterPatientInput): Promise<RegisterPatientResponse> {
|
||||||
try {
|
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
|
// 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;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error("[patientService.register] ❌ Erro completo:", {
|
console.error("[patientService.register] Erro completo:", {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
response: error.response?.data,
|
response: error.response?.data,
|
||||||
status: error.response?.status,
|
status: error.response?.status,
|
||||||
statusText: error.response?.statusText,
|
|
||||||
data: error.response?.data,
|
|
||||||
});
|
});
|
||||||
|
throw error;
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
|
|
||||||
export interface Patient {
|
export interface Patient {
|
||||||
id?: string;
|
id?: string;
|
||||||
user_id?: string;
|
|
||||||
full_name: string;
|
full_name: string;
|
||||||
cpf: string;
|
cpf: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -29,7 +28,6 @@ export interface Patient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CreatePatientInput {
|
export interface CreatePatientInput {
|
||||||
user_id?: string;
|
|
||||||
full_name: string;
|
full_name: string;
|
||||||
cpf: string;
|
cpf: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@ -35,8 +35,13 @@ class ReportService {
|
|||||||
* Busca relatório por ID
|
* Busca relatório por ID
|
||||||
*/
|
*/
|
||||||
async getById(id: string): Promise<Report> {
|
async getById(id: string): Promise<Report> {
|
||||||
const response = await apiClient.get<Report>(`${this.basePath}/${id}`);
|
const response = await apiClient.get<Report[]>(
|
||||||
return response.data;
|
`${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
|
* Nota: order_number não pode ser modificado
|
||||||
*/
|
*/
|
||||||
async update(id: string, data: UpdateReportInput): Promise<Report> {
|
async update(id: string, data: UpdateReportInput): Promise<Report> {
|
||||||
const response = await apiClient.patch<Report>(
|
const response = await apiClient.patch<Report[]>(
|
||||||
`${this.basePath}/${id}`,
|
`${this.basePath}?id=eq.${id}`,
|
||||||
data
|
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;
|
email: string;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
phone?: string | null;
|
phone?: string | null;
|
||||||
|
password?: string; // Senha para registro direto (opcional - se não fornecida, envia magic link)
|
||||||
role: UserRole; // Agora é obrigatório
|
role: UserRole; // Agora é obrigatório
|
||||||
create_patient_record?: boolean; // Novo campo opcional
|
create_patient_record?: boolean; // Novo campo opcional
|
||||||
cpf?: string; // Obrigatório se create_patient_record=true
|
cpf?: string; // Obrigatório se create_patient_record=true
|
||||||
|
|||||||
@ -2,7 +2,9 @@
|
|||||||
* Serviço de Usuários
|
* Serviço de Usuários
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
import { apiClient } from "../api/client";
|
import { apiClient } from "../api/client";
|
||||||
|
import { API_CONFIG } from "../api/config";
|
||||||
import type {
|
import type {
|
||||||
UserRoleRecord,
|
UserRoleRecord,
|
||||||
UserInfo,
|
UserInfo,
|
||||||
@ -20,7 +22,7 @@ class UserService {
|
|||||||
* Lista roles de usuários
|
* Lista roles de usuários
|
||||||
*/
|
*/
|
||||||
async listRoles(): Promise<UserRoleRecord[]> {
|
async listRoles(): Promise<UserRoleRecord[]> {
|
||||||
const response = await apiClient.get<UserRoleRecord[]>("/user-roles");
|
const response = await apiClient.get<UserRoleRecord[]>("/user_roles");
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,7 +31,24 @@ class UserService {
|
|||||||
* Inclui perfil, roles e permissões calculadas
|
* Inclui perfil, roles e permissões calculadas
|
||||||
*/
|
*/
|
||||||
async getUserInfo(): Promise<UserInfo> {
|
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;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,29 +56,84 @@ class UserService {
|
|||||||
* Obtém dados básicos do usuário autenticado
|
* Obtém dados básicos do usuário autenticado
|
||||||
*/
|
*/
|
||||||
async getCurrentUser(): Promise<User> {
|
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;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cria novo usuário no sistema
|
* Cria novo usuário no sistema
|
||||||
* Pode ser chamado COM ou SEM autenticação:
|
* Pode ser chamado COM ou SEM autenticação:
|
||||||
* - SEM autenticação: auto-registro público (médico, paciente)
|
* - SEM autenticação: usa signup nativo (/auth/v1/signup) - PÚBLICO
|
||||||
* - COM autenticação: criação por admin/secretária
|
* - COM autenticação: usa Edge Function (/functions/v1/create-user) - ADMIN
|
||||||
* Envia magic link automaticamente para o email
|
|
||||||
*/
|
*/
|
||||||
async createUser(
|
async createUser(
|
||||||
data: CreateUserInput,
|
data: CreateUserInput,
|
||||||
isPublicRegistration: boolean = true
|
isPublicRegistration: boolean = true
|
||||||
): Promise<CreateUserResponse> {
|
): Promise<CreateUserResponse> {
|
||||||
// Se for registro público, usa postPublic (sem token)
|
// Registro público: usar endpoint nativo do Supabase
|
||||||
// Se for criação por admin, usa post normal (com token)
|
if (isPublicRegistration) {
|
||||||
const response = isPublicRegistration
|
const response = await axios.post<{
|
||||||
? await (apiClient as any).postPublic<CreateUserResponse>(
|
user: User;
|
||||||
"/create-user",
|
session: { access_token: string; refresh_token: string };
|
||||||
data
|
}>(
|
||||||
)
|
`${API_CONFIG.AUTH_URL}/signup`,
|
||||||
: await apiClient.post<CreateUserResponse>("/create-user", data);
|
{
|
||||||
|
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;
|
return response.data;
|
||||||
}
|
}
|
||||||
@ -69,7 +143,7 @@ class UserService {
|
|||||||
* Requer permissão de admin
|
* Requer permissão de admin
|
||||||
*/
|
*/
|
||||||
async addUserRole(userId: string, role: string): Promise<UserRoleRecord> {
|
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,
|
user_id: userId,
|
||||||
role: role,
|
role: role,
|
||||||
});
|
});
|
||||||
@ -81,7 +155,7 @@ class UserService {
|
|||||||
* Requer permissão de admin
|
* Requer permissão de admin
|
||||||
*/
|
*/
|
||||||
async removeUserRole(userId: string, role: string): Promise<void> {
|
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
|
* Use quando tiver TODOS os dados do médico
|
||||||
*/
|
*/
|
||||||
async createDoctor(data: CreateDoctorInput): Promise<CreateDoctorResponse> {
|
async createDoctor(data: CreateDoctorInput): Promise<CreateDoctorResponse> {
|
||||||
const response = await apiClient.post<CreateDoctorResponse>(
|
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
|
||||||
"/create-doctor",
|
|
||||||
data
|
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;
|
return response.data;
|
||||||
}
|
}
|
||||||
@ -109,15 +192,25 @@ class UserService {
|
|||||||
async createPatient(
|
async createPatient(
|
||||||
data: CreatePatientInput
|
data: CreatePatientInput
|
||||||
): Promise<CreatePatientResponse> {
|
): Promise<CreatePatientResponse> {
|
||||||
const response = await apiClient.post<CreatePatientResponse>(
|
const token = localStorage.getItem(API_CONFIG.STORAGE_KEYS.ACCESS_TOKEN);
|
||||||
"/create-patient",
|
|
||||||
data
|
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;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cria usuário com email e senha (alternativa ao Magic Link)
|
* 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
|
* Requer permissão de admin, gestor ou secretaria
|
||||||
* O usuário precisa confirmar o email antes de fazer login
|
* O usuário precisa confirmar o email antes de fazer login
|
||||||
*/
|
*/
|
||||||
@ -137,15 +230,31 @@ class UserService {
|
|||||||
email: string;
|
email: string;
|
||||||
full_name: string;
|
full_name: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
|
email_confirmed_at: string | null;
|
||||||
|
patient_id?: string;
|
||||||
};
|
};
|
||||||
message: 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;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deleta usuário permanentemente (Hard Delete)
|
* Deleta usuário permanentemente (Hard Delete)
|
||||||
|
* POST /delete-user
|
||||||
* ⚠️ OPERAÇÃO IRREVERSÍVEL! Use apenas em desenvolvimento/QA
|
* ⚠️ OPERAÇÃO IRREVERSÍVEL! Use apenas em desenvolvimento/QA
|
||||||
* Requer permissão de admin ou gestor
|
* Requer permissão de admin ou gestor
|
||||||
* Deleta em cascata: profiles, user_roles, doctors, patients, etc.
|
* Deleta em cascata: profiles, user_roles, doctors, patients, etc.
|
||||||
@ -155,7 +264,51 @@ class UserService {
|
|||||||
message: string;
|
message: string;
|
||||||
userId: 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;
|
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)",
|
md: "calc(var(--radius) - 2px)",
|
||||||
sm: "calc(var(--radius) - 4px)",
|
sm: "calc(var(--radius) - 4px)",
|
||||||
},
|
},
|
||||||
|
transitionDuration: {
|
||||||
|
2000: "2000ms",
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
bounce: "bounce 1s infinite",
|
||||||
|
},
|
||||||
|
animationDelay: {
|
||||||
|
100: "100ms",
|
||||||
|
200: "200ms",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
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