Merge pull request #16 from m1guelmcf/feature/auto-cadastro-paciente
feat: Corrige e implementa o fluxo de auto-cadastro de paciente
This commit is contained in:
commit
e31d7f7046
@ -1,7 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import type React from "react"
|
import type React from "react"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
@ -9,24 +8,24 @@ import { Button } from "@/components/ui/button"
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { ArrowLeft, Loader2 } from "lucide-react"
|
||||||
import { Eye, EyeOff, ArrowLeft } from "lucide-react"
|
import { useToast } from "@/hooks/use-toast"
|
||||||
|
import { usersService } from "@/services/usersApi.mjs" // Mantém a importação
|
||||||
|
import { isValidCPF } from "@/lib/utils"
|
||||||
|
|
||||||
export default function PatientRegister() {
|
export default function PatientRegister() {
|
||||||
const [showPassword, setShowPassword] = useState(false)
|
// REMOVIDO: Estados para 'showPassword' e 'showConfirmPassword'
|
||||||
const [showConfirmPassword, setShowConfirmPassword] = useState(false)
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: "",
|
name: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
|
||||||
confirmPassword: "",
|
|
||||||
phone: "",
|
phone: "",
|
||||||
cpf: "",
|
cpf: "",
|
||||||
birthDate: "",
|
birthDate: "",
|
||||||
address: "",
|
// REMOVIDO: Campos 'password' e 'confirmPassword'
|
||||||
})
|
})
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { toast } = useToast()
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: string) => {
|
const handleInputChange = (field: string, value: string) => {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
@ -37,22 +36,52 @@ export default function PatientRegister() {
|
|||||||
|
|
||||||
const handleRegister = async (e: React.FormEvent) => {
|
const handleRegister = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
if (formData.password !== formData.confirmPassword) {
|
// --- VALIDAÇÃO DE CPF ---
|
||||||
alert("As senhas não coincidem!")
|
if (!isValidCPF(formData.cpf)) {
|
||||||
|
toast({
|
||||||
|
title: "CPF Inválido",
|
||||||
|
description: "O CPF informado não é válido. Verifique os dígitos.",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
setIsLoading(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsLoading(true)
|
// --- LÓGICA DE REGISTRO COM ENDPOINT PÚBLICO ---
|
||||||
|
try {
|
||||||
|
// ALTERADO: Payload ajustado para o endpoint 'register-patient'
|
||||||
|
const payload = {
|
||||||
|
email: formData.email.trim().toLowerCase(),
|
||||||
|
full_name: formData.name,
|
||||||
|
phone_mobile: formData.phone, // O endpoint espera 'phone_mobile'
|
||||||
|
cpf: formData.cpf.replace(/\D/g, ''),
|
||||||
|
birth_date: formData.birthDate,
|
||||||
|
}
|
||||||
|
|
||||||
// Simulação de registro - em produção, conectar com API real
|
// ALTERADO: Chamada para a nova função de serviço
|
||||||
setTimeout(() => {
|
await usersService.registerPatient(payload)
|
||||||
// Salvar dados do usuário no localStorage para simulação
|
|
||||||
const { confirmPassword, ...userData } = formData
|
// ALTERADO: Mensagem de sucesso para refletir o fluxo de confirmação por e-mail
|
||||||
localStorage.setItem("patientData", JSON.stringify(userData))
|
toast({
|
||||||
router.push("/patient/dashboard")
|
title: "Cadastro enviado com sucesso!",
|
||||||
|
description: "Enviamos um link de confirmação para o seu e-mail. Por favor, verifique sua caixa de entrada para ativar sua conta.",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Redireciona para a página de login
|
||||||
|
router.push("/login")
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Erro no registro:", error)
|
||||||
|
toast({
|
||||||
|
title: "Erro ao Criar Conta",
|
||||||
|
description: error.message || "Não foi possível concluir o cadastro. Verifique seus dados e tente novamente.",
|
||||||
|
variant: "destructive",
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}, 1000)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -67,136 +96,85 @@ export default function PatientRegister() {
|
|||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="text-center">
|
<CardHeader className="text-center">
|
||||||
<CardTitle className="text-2xl">Cadastro de Paciente</CardTitle>
|
<CardTitle className="text-2xl">Crie sua Conta de Paciente</CardTitle>
|
||||||
<CardDescription>Preencha seus dados para criar sua conta</CardDescription>
|
<CardDescription>Preencha seus dados para acessar o portal MedConnect</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleRegister} className="space-y-4">
|
<form onSubmit={handleRegister} className="space-y-4">
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="name">Nome Completo</Label>
|
<Label htmlFor="name">Nome Completo *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="name"
|
id="name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={(e) => handleInputChange("name", e.target.value)}
|
onChange={(e) => handleInputChange("name", e.target.value)}
|
||||||
required
|
required
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="cpf">CPF</Label>
|
<Label htmlFor="cpf">CPF *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="cpf"
|
id="cpf"
|
||||||
value={formData.cpf}
|
value={formData.cpf}
|
||||||
onChange={(e) => handleInputChange("cpf", e.target.value)}
|
onChange={(e) => handleInputChange("cpf", e.target.value)}
|
||||||
placeholder="000.000.000-00"
|
placeholder="000.000.000-00"
|
||||||
required
|
required
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
<div className="grid md:grid-cols-2 gap-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email">Email</Label>
|
<Label htmlFor="email">Email *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => handleInputChange("email", e.target.value)}
|
onChange={(e) => handleInputChange("email", e.target.value)}
|
||||||
required
|
required
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="phone">Telefone</Label>
|
<Label htmlFor="phone">Telefone *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="phone"
|
id="phone"
|
||||||
value={formData.phone}
|
value={formData.phone}
|
||||||
onChange={(e) => handleInputChange("phone", e.target.value)}
|
onChange={(e) => handleInputChange("phone", e.target.value)}
|
||||||
placeholder="(11) 99999-9999"
|
placeholder="(11) 99999-9999"
|
||||||
required
|
required
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="birthDate">Data de Nascimento</Label>
|
<Label htmlFor="birthDate">Data de Nascimento *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="birthDate"
|
id="birthDate"
|
||||||
type="date"
|
type="date"
|
||||||
value={formData.birthDate}
|
value={formData.birthDate}
|
||||||
onChange={(e) => handleInputChange("birthDate", e.target.value)}
|
onChange={(e) => handleInputChange("birthDate", e.target.value)}
|
||||||
required
|
required
|
||||||
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
{/* REMOVIDO: Seção de senha e confirmação de senha */}
|
||||||
<Label htmlFor="address">Endereço</Label>
|
|
||||||
<Textarea
|
|
||||||
id="address"
|
|
||||||
value={formData.address}
|
|
||||||
onChange={(e) => handleInputChange("address", e.target.value)}
|
|
||||||
placeholder="Rua, número, bairro, cidade, estado"
|
|
||||||
rows={3}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="password">Senha</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
value={formData.password}
|
|
||||||
onChange={(e) => handleInputChange("password", e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
|
||||||
onClick={() => setShowPassword(!showPassword)}
|
|
||||||
>
|
|
||||||
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="confirmPassword">Confirmar Senha</Label>
|
|
||||||
<div className="relative">
|
|
||||||
<Input
|
|
||||||
id="confirmPassword"
|
|
||||||
type={showConfirmPassword ? "text" : "password"}
|
|
||||||
value={formData.confirmPassword}
|
|
||||||
onChange={(e) => handleInputChange("confirmPassword", e.target.value)}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
|
||||||
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
|
|
||||||
>
|
|
||||||
{showConfirmPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||||
{isLoading ? "Criando conta..." : "Criar Conta"}
|
{isLoading ? <><Loader2 className="mr-2 h-4 w-4 animate-spin" /> Criando conta...</> : "Criar Conta"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-6 text-center">
|
<div className="mt-6 text-center">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
Já tem uma conta?{" "}
|
Já tem uma conta?{" "}
|
||||||
<Link href="/patient/login" className="text-blue-600 hover:underline">
|
<Link href="/login" className="text-blue-600 hover:underline">
|
||||||
Faça login aqui
|
Faça login aqui
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -1,100 +1,98 @@
|
|||||||
import { api } from "./api.mjs";
|
import { api } from "./api.mjs";
|
||||||
|
|
||||||
export const usersService = {
|
export const usersService = {
|
||||||
// Função getMe corrigida para chamar a si mesma pelo nome
|
// Função getMe corrigida para chamar a si mesma pelo nome
|
||||||
async getMe() {
|
async getMe() {
|
||||||
const sessionData = await api.getSession();
|
const sessionData = await api.getSession();
|
||||||
if (!sessionData?.id) {
|
if (!sessionData?.id) {
|
||||||
console.error("Sessão não encontrada ou usuário sem ID.", sessionData);
|
console.error("Sessão não encontrada ou usuário sem ID.", sessionData);
|
||||||
throw new Error("Usuário não autenticado.");
|
throw new Error("Usuário não autenticado.");
|
||||||
}
|
}
|
||||||
// Chamando a outra função do serviço pelo nome explícito
|
// Chamando a outra função do serviço pelo nome explícito
|
||||||
return usersService.full_data(sessionData.id);
|
return usersService.full_data(sessionData.id);
|
||||||
},
|
},
|
||||||
|
|
||||||
async list_roles() {
|
async list_roles() {
|
||||||
return await api.get(`/rest/v1/user_roles?select=id,user_id,role,created_at`);
|
return await api.get(`/rest/v1/user_roles?select=id,user_id,role,created_at`);
|
||||||
},
|
},
|
||||||
|
|
||||||
async create_user(data) {
|
async create_user(data) {
|
||||||
// Esta é a função usada no page.tsx para criar usuários que não são médicos
|
// Esta é a função usada no page.tsx para criar usuários que não são médicos
|
||||||
return await api.post(`/functions/v1/create-user-with-password`, data);
|
return await api.post(`/functions/v1/create-user-with-password`, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getMeSimple() {
|
// --- NOVA FUNÇÃO ADICIONADA AQUI ---
|
||||||
return await api.post(`/functions/v1/user-info`);
|
// Esta função chama o endpoint público de registro de paciente.
|
||||||
},
|
async registerPatient(data) {
|
||||||
|
// POR QUÊ? Este endpoint é público e não requer token JWT, resolvendo o erro 401.
|
||||||
|
return await api.post("/functions/v1/register-patient", data);
|
||||||
|
},
|
||||||
|
// --- FIM DA NOVA FUNÇÃO ---
|
||||||
|
|
||||||
async full_data(user_id) {
|
async getMeSimple() {
|
||||||
if (!user_id) throw new Error("user_id é obrigatório");
|
return await api.post(`/functions/v1/user-info`);
|
||||||
|
},
|
||||||
|
|
||||||
const [profile] = await api.get(`/rest/v1/profiles?id=eq.${user_id}`);
|
async full_data(user_id) {
|
||||||
const [role] = await api.get(`/rest/v1/user_roles?user_id=eq.${user_id}`);
|
if (!user_id) throw new Error("user_id é obrigatório");
|
||||||
const permissions = {
|
|
||||||
isAdmin: role?.role === "admin",
|
|
||||||
isManager: role?.role === "gestor",
|
|
||||||
isDoctor: role?.role === "medico",
|
|
||||||
isSecretary: role?.role === "secretaria",
|
|
||||||
isAdminOrManager:
|
|
||||||
role?.role === "admin" || role?.role === "gestor" ? true : false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
const [profile] = await api.get(`/rest/v1/profiles?id=eq.${user_id}`);
|
||||||
user: {
|
const [role] = await api.get(`/rest/v1/user_roles?user_id=eq.${user_id}`);
|
||||||
id: user_id,
|
const permissions = {
|
||||||
email: profile?.email ?? "—",
|
isAdmin: role?.role === "admin",
|
||||||
email_confirmed_at: null,
|
isManager: role?.role === "gestor",
|
||||||
created_at: profile?.created_at ?? "—",
|
isDoctor: role?.role === "medico",
|
||||||
last_sign_in_at: null,
|
isSecretary: role?.role === "secretaria",
|
||||||
},
|
isAdminOrManager: role?.role === "admin" || role?.role === "gestor" ? true : false,
|
||||||
profile: {
|
};
|
||||||
id: profile?.id ?? user_id,
|
|
||||||
full_name: profile?.full_name ?? "—",
|
|
||||||
email: profile?.email ?? "—",
|
|
||||||
phone: profile?.phone ?? "—",
|
|
||||||
avatar_url: profile?.avatar_url ?? null,
|
|
||||||
disabled: profile?.disabled ?? false,
|
|
||||||
created_at: profile?.created_at ?? null,
|
|
||||||
updated_at: profile?.updated_at ?? null,
|
|
||||||
},
|
|
||||||
roles: [role?.role ?? "—"],
|
|
||||||
permissions,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
async resetPassword(email) {
|
|
||||||
if (!email) throw new Error("Email é obrigatório para resetar a senha.");
|
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
id: user_id,
|
||||||
|
email: profile?.email ?? "—",
|
||||||
|
email_confirmed_at: null,
|
||||||
|
created_at: profile?.created_at ?? "—",
|
||||||
|
last_sign_in_at: null,
|
||||||
|
},
|
||||||
|
profile: {
|
||||||
|
id: profile?.id ?? user_id,
|
||||||
|
full_name: profile?.full_name ?? "—",
|
||||||
|
email: profile?.email ?? "—",
|
||||||
|
phone: profile?.phone ?? "—",
|
||||||
|
avatar_url: profile?.avatar_url ?? null,
|
||||||
|
disabled: profile?.disabled ?? false,
|
||||||
|
created_at: profile?.created_at ?? null,
|
||||||
|
updated_at: profile?.updated_at ?? null,
|
||||||
|
},
|
||||||
|
roles: [role?.role ?? "—"],
|
||||||
|
permissions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async resetPassword(email) {
|
||||||
|
if (!email) throw new Error("Email é obrigatório para resetar a senha.");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(
|
const res = await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/recover`, {
|
||||||
`${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/recover`,
|
method: "POST",
|
||||||
{
|
headers: {
|
||||||
method: "POST",
|
"Content-Type": "application/json",
|
||||||
headers: {
|
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
||||||
"Content-Type": "application/json",
|
},
|
||||||
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
|
body: JSON.stringify({ email }),
|
||||||
},
|
});
|
||||||
body: JSON.stringify({ email }),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
|
||||||
const data = await res.json().catch(() => ({}));
|
if (!res.ok) {
|
||||||
|
console.error("Erro no resetPassword:", res.status, data);
|
||||||
|
throw new Error(`Erro ${res.status}: ${data.message || "Falha ao resetar senha."}`);
|
||||||
if (!res.ok) {
|
}
|
||||||
console.error("Erro no resetPassword:", res.status, data);
|
|
||||||
throw new Error(`Erro ${res.status}: ${data.message || "Falha ao resetar senha."}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
console.log("✅ Reset de senha:", data);
|
|
||||||
return data;
|
|
||||||
} catch (err) {
|
|
||||||
console.error("❌ Erro na chamada resetPassword:", err);
|
|
||||||
throw new Error(err.message || "Erro inesperado na recuperação de senha.");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log("✅ Reset de senha:", data);
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("❌ Erro na chamada resetPassword:", err);
|
||||||
|
throw new Error(err.message || "Erro inesperado na recuperação de senha.");
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user