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:
DaniloSts 2025-11-13 16:45:59 -03:00 committed by GitHub
commit e31d7f7046
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 146 additions and 170 deletions

View File

@ -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">
tem uma conta?{" "} 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>

View File

@ -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.");
}
},
}; };