diff --git a/susconecta/app/login-paciente/page.tsx b/susconecta/app/login-paciente/page.tsx index 04f83b3..75e4b2e 100644 --- a/susconecta/app/login-paciente/page.tsx +++ b/susconecta/app/login-paciente/page.tsx @@ -33,7 +33,16 @@ export default function LoginPacientePage() { console.error('[LOGIN-PACIENTE] Erro no login:', err) if (err instanceof AuthenticationError) { - setError(err.message) + // Verificar se é erro de credenciais inválidas (pode ser email não confirmado) + if (err.code === '400' || err.details?.error_code === 'invalid_credentials') { + setError( + '⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' + + 'verifique sua caixa de entrada e clique no link de confirmação ' + + 'que foi enviado para ' + credentials.email + ) + } else { + setError(err.message) + } } else { setError('Erro inesperado. Tente novamente.') } diff --git a/susconecta/app/login/page.tsx b/susconecta/app/login/page.tsx index dba68b1..f39edb6 100644 --- a/susconecta/app/login/page.tsx +++ b/susconecta/app/login/page.tsx @@ -35,7 +35,16 @@ export default function LoginPage() { console.error('[LOGIN-PROFISSIONAL] Erro no login:', err) if (err instanceof AuthenticationError) { - setError(err.message) + // Verificar se é erro de credenciais inválidas (pode ser email não confirmado) + if (err.code === '400' || err.details?.error_code === 'invalid_credentials') { + setError( + '⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' + + 'verifique sua caixa de entrada e clique no link de confirmação ' + + 'que foi enviado para ' + credentials.email + ) + } else { + setError(err.message) + } } else { setError('Erro inesperado. Tente novamente.') } diff --git a/susconecta/components/credentials-dialog.tsx b/susconecta/components/credentials-dialog.tsx new file mode 100644 index 0000000..8a1cec8 --- /dev/null +++ b/susconecta/components/credentials-dialog.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { useState } from "react"; +import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; +import { CheckCircle2, Copy, Eye, EyeOff } from "lucide-react"; +import { Alert, AlertDescription } from "@/components/ui/alert"; + +export interface CredentialsDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + email: string; + password: string; + userName: string; + userType: "médico" | "paciente"; +} + +export function CredentialsDialog({ + open, + onOpenChange, + email, + password, + userName, + userType, +}: CredentialsDialogProps) { + const [showPassword, setShowPassword] = useState(false); + const [copiedEmail, setCopiedEmail] = useState(false); + const [copiedPassword, setCopiedPassword] = useState(false); + + function handleCopyEmail() { + navigator.clipboard.writeText(email); + setCopiedEmail(true); + setTimeout(() => setCopiedEmail(false), 2000); + } + + function handleCopyPassword() { + navigator.clipboard.writeText(password); + setCopiedPassword(true); + setTimeout(() => setCopiedPassword(false), 2000); + } + + function handleCopyBoth() { + const text = `Email: ${email}\nSenha: ${password}`; + navigator.clipboard.writeText(text); + } + + return ( + + + + + + {userType === "médico" ? "Médico" : "Paciente"} Cadastrado com Sucesso! + + + O {userType} {userName} foi cadastrado e pode fazer login com as credenciais abaixo. + + + + + + Importante: Anote ou copie estas credenciais agora. Por segurança, essa senha não será exibida novamente. + + + + + + 📧 Confirme o email: Um email de confirmação foi enviado para {email}. + O {userType} deve clicar no link de confirmação antes de fazer o primeiro login. + + + +
+
+ +
+ + +
+
+ +
+ +
+
+ + +
+ +
+
+
+ +
+ Próximos passos: +
    +
  1. Compartilhe estas credenciais com o {userType}
  2. +
  3. + O {userType} deve confirmar o email clicando no link enviado para{" "} + {email} (verifique também a pasta de spam) +
  4. +
  5. + Após confirmar o email, o {userType} deve acessar:{" "} + + {userType === "médico" ? "/login" : "/login-paciente"} + +
  6. +
  7. + Após o login, terá acesso à área:{" "} + + {userType === "médico" ? "/profissional" : "/paciente"} + +
  8. +
  9. Recomende trocar a senha no primeiro acesso
  10. +
+
+ + + + + +
+
+ ); +} diff --git a/susconecta/components/forms/doctor-registration-form.tsx b/susconecta/components/forms/doctor-registration-form.tsx index ab80830..13251e7 100644 --- a/susconecta/components/forms/doctor-registration-form.tsx +++ b/susconecta/components/forms/doctor-registration-form.tsx @@ -24,10 +24,13 @@ import { removerAnexoMedico, MedicoInput, // 👈 importado do lib/api Medico, // 👈 adicionado import do tipo Medico + criarUsuarioMedico, + CreateUserWithPasswordResponse, } from "@/lib/api"; ; -import { buscarCepAPI } from "@/lib/api"; +import { buscarCepAPI } from "@/lib/api"; +import { CredentialsDialog } from "@/components/credentials-dialog"; type FormacaoAcademica = { instituicao: string; @@ -150,6 +153,11 @@ export function DoctorRegistrationForm({ const [isSearchingCEP, setSearchingCEP] = useState(false); const [photoPreview, setPhotoPreview] = useState(null); const [serverAnexos, setServerAnexos] = useState([]); + + // Estados para o dialog de credenciais + const [showCredentials, setShowCredentials] = useState(false); + const [credentials, setCredentials] = useState(null); + const [savedDoctor, setSavedDoctor] = useState(null); const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]); @@ -391,8 +399,69 @@ if (missingFields.length > 0) { console.log("✅ Médico salvo com sucesso:", saved); - onSaved?.(saved); - setSubmitting(false); + // Se for criação de novo médico e tiver email válido, cria usuário + if (mode === "create" && form.email && form.email.includes('@')) { + console.log("🔐 Iniciando criação de usuário para o médico..."); + console.log("📧 Email:", form.email); + console.log("👤 Nome:", form.full_name); + console.log("📱 Telefone:", form.celular); + + try { + const userCredentials = await criarUsuarioMedico({ + email: form.email, + full_name: form.full_name, + phone_mobile: form.celular, + }); + + console.log("✅ Usuário criado com sucesso!", userCredentials); + console.log("🔑 Senha gerada:", userCredentials.password); + + // Armazena as credenciais e mostra o dialog + setCredentials(userCredentials); + setShowCredentials(true); + setSavedDoctor(saved); // Salva médico para chamar onSaved depois + + console.log("📋 Credenciais definidas, dialog deve aparecer!"); + + // NÃO chama onSaved aqui! Isso fecha o formulário. + // O dialog vai chamar onSaved quando o usuário fechar + setSubmitting(false); + return; // ← IMPORTANTE: Impede que o código abaixo seja executado + + } catch (userError: any) { + console.error("❌ ERRO ao criar usuário:", userError); + console.error("📋 Stack trace:", userError?.stack); + const errorMessage = userError?.message || "Erro desconhecido"; + console.error("💬 Mensagem:", errorMessage); + + // Mostra erro mas fecha o formulário normalmente + alert(`Médico cadastrado com sucesso!\n\n⚠️ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`); + + // Fecha o formulário mesmo com erro na criação de usuário + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + onSaved?.(saved); + if (inline) onClose?.(); + else onOpenChange?.(false); + setSubmitting(false); + return; + } + } else { + console.log("⚠️ Não criará usuário. Motivo:"); + console.log(" - Mode:", mode); + console.log(" - Email:", form.email); + console.log(" - Tem @:", form.email?.includes('@')); + + // Se não for criar usuário, fecha normalmente + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + onSaved?.(saved); + if (inline) onClose?.(); + else onOpenChange?.(false); + setSubmitting(false); + } } catch (err: any) { console.error("❌ Erro ao salvar médico:", err); console.error("❌ Detalhes do erro:", { @@ -935,18 +1004,90 @@ if (missingFields.length > 0) { ); - if (inline) return
{content}
; + if (inline) { + return ( + <> +
{content}
+ + {/* Dialog de credenciais */} + {credentials && ( + { + console.log("🔄 CredentialsDialog (inline) onOpenChange:", open); + setShowCredentials(open); + if (!open) { + console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário"); + + // Chama onSaved com o médico salvo + if (savedDoctor) { + console.log("✅ Chamando onSaved com médico:", savedDoctor.id); + onSaved?.(savedDoctor); + } + + // Limpa o formulário e fecha + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + setCredentials(null); + setSavedDoctor(null); + onClose?.(); + } + }} + email={credentials.email} + password={credentials.password} + userName={form.full_name} + userType="médico" + /> + )} + + ); + } return ( - - - - - {title} - - - {content} - - + <> + + + + + {title} + + + {content} + + + + {/* Dialog de credenciais */} + {credentials && ( + { + console.log("🔄 CredentialsDialog (dialog mode) onOpenChange:", open); + setShowCredentials(open); + if (!open) { + console.log("🔄 Dialog fechando - chamando onSaved e fechando modal principal"); + + // Chama onSaved com o médico salvo + if (savedDoctor) { + console.log("✅ Chamando onSaved com médico:", savedDoctor.id); + onSaved?.(savedDoctor); + } + + // Limpa o formulário e fecha o modal principal + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + setCredentials(null); + setSavedDoctor(null); + onOpenChange?.(false); + } + }} + email={credentials.email} + password={credentials.password} + userName={form.full_name} + userType="médico" + /> + )} + ); } diff --git a/susconecta/components/forms/patient-registration-form.tsx b/susconecta/components/forms/patient-registration-form.tsx index 7c16def..1abade4 100644 --- a/susconecta/components/forms/patient-registration-form.tsx +++ b/susconecta/components/forms/patient-registration-form.tsx @@ -25,10 +25,13 @@ import { listarAnexos, removerAnexo, buscarPacientePorId, + criarUsuarioPaciente, + CreateUserWithPasswordResponse, } from "@/lib/api"; import { validarCPFLocal } from "@/lib/utils"; -import { verificarCpfDuplicado } from "@/lib/api"; +import { verificarCpfDuplicado } from "@/lib/api"; +import { CredentialsDialog } from "@/components/credentials-dialog"; @@ -104,6 +107,11 @@ export function PatientRegistrationForm({ const [isSearchingCEP, setSearchingCEP] = useState(false); const [photoPreview, setPhotoPreview] = useState(null); const [serverAnexos, setServerAnexos] = useState([]); + + // Estados para o dialog de credenciais + const [showCredentials, setShowCredentials] = useState(false); + const [credentials, setCredentials] = useState(null); + const [savedPatient, setSavedPatient] = useState(null); const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]); @@ -264,15 +272,88 @@ export function PatientRegistrationForm({ } } + // Se for criação de novo paciente e tiver email válido, cria usuário + if (mode === "create" && form.email && form.email.includes('@')) { + console.log("🔐 Iniciando criação de usuário para o paciente..."); + console.log("📧 Email:", form.email); + console.log("👤 Nome:", form.nome); + console.log("📱 Telefone:", form.telefone); + + try { + const userCredentials = await criarUsuarioPaciente({ + email: form.email, + full_name: form.nome, + phone_mobile: form.telefone, + }); + + console.log("✅ Usuário criado com sucesso!", userCredentials); + console.log("🔑 Senha gerada:", userCredentials.password); + + // Armazena as credenciais e mostra o dialog + console.log("📋 Antes de setCredentials - credentials atual:", credentials); + console.log("📋 Antes de setShowCredentials - showCredentials atual:", showCredentials); + + setCredentials(userCredentials); + setShowCredentials(true); + + console.log("📋 Depois de set - credentials:", userCredentials); + console.log("📋 Depois de set - showCredentials: true"); + console.log("📋 Modo inline?", inline); + console.log("📋 userCredentials completo:", JSON.stringify(userCredentials)); + + // Força re-render + setTimeout(() => { + console.log("⏰ Timeout - credentials:", credentials); + console.log("⏰ Timeout - showCredentials:", showCredentials); + }, 100); + + console.log("📋 Credenciais definidas, dialog deve aparecer!"); + + // Salva o paciente para chamar onSaved depois + setSavedPatient(saved); + + // ⚠️ NÃO chama onSaved aqui! O dialog vai chamar quando fechar. + // Se chamar agora, o formulário fecha e o dialog desaparece. + console.log("⚠️ NÃO chamando onSaved ainda - aguardando dialog fechar"); + + // RETORNA AQUI para não executar o código abaixo + return; + + } catch (userError: any) { + console.error("❌ ERRO ao criar usuário:", userError); + console.error("📋 Stack trace:", userError?.stack); + const errorMessage = userError?.message || "Erro desconhecido"; + console.error("� Mensagem:", errorMessage); + + // Mostra erro mas fecha o formulário normalmente + alert(`Paciente cadastrado com sucesso!\n\n⚠️ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`); + + // Fecha o formulário mesmo com erro na criação de usuário + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + + if (inline) onClose?.(); + else onOpenChange?.(false); + } + } else { + console.log("⚠️ Não criará usuário. Motivo:"); + console.log(" - Mode:", mode); + console.log(" - Email:", form.email); + console.log(" - Tem @:", form.email?.includes('@')); + + // Se não for criar usuário, fecha normalmente + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + + if (inline) onClose?.(); + else onOpenChange?.(false); + + alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!"); + } + onSaved?.(saved); - setForm(initial); - setPhotoPreview(null); - setServerAnexos([]); - - if (inline) onClose?.(); - else onOpenChange?.(false); - - alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!"); } catch (err: any) { setErrors({ submit: err?.message || "Erro ao salvar paciente." }); } finally { @@ -611,18 +692,85 @@ export function PatientRegistrationForm({ ); - if (inline) return
{content}
; + if (inline) { + return ( + <> +
{content}
+ + {/* Debug */} + {console.log("🎨 RENDER inline - credentials:", credentials, "showCredentials:", showCredentials)} + + {/* Dialog de credenciais */} + {credentials && ( + { + console.log("🔄 CredentialsDialog onOpenChange:", open); + setShowCredentials(open); + if (!open) { + console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário"); + + // Chama onSaved com o paciente salvo + if (savedPatient) { + console.log("✅ Chamando onSaved com paciente:", savedPatient.id); + onSaved?.(savedPatient); + } + + // Limpa o formulário e fecha + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + setCredentials(null); + setSavedPatient(null); + onClose?.(); + } + }} + email={credentials.email} + password={credentials.password} + userName={form.nome} + userType="paciente" + /> + )} + + ); + } return ( - - - - - {title} - - - {content} - - + <> + {console.log("🎨 RENDER dialog - credentials:", credentials, "showCredentials:", showCredentials)} + + + + + + {title} + + + {content} + + + + {/* Dialog de credenciais */} + {credentials && ( + { + setShowCredentials(open); + if (!open) { + // Quando fechar o dialog, limpa o formulário e fecha o modal principal + setForm(initial); + setPhotoPreview(null); + setServerAnexos([]); + setCredentials(null); + onOpenChange?.(false); + } + }} + email={credentials.email} + password={credentials.password} + userName={form.nome} + userType="paciente" + /> + )} + ); } diff --git a/susconecta/eslint.config.js b/susconecta/eslint.config.js index 90b0b03..6438c77 100644 --- a/susconecta/eslint.config.js +++ b/susconecta/eslint.config.js @@ -5,31 +5,84 @@ import eslint from "@eslint/js"; import nextPlugin from "@next/eslint-plugin-next"; import unicornPlugin from "eslint-plugin-unicorn"; import prettierConfig from "eslint-config-prettier"; +import { FlatCompat } from "@eslint/eslintrc"; +import { fileURLToPath } from "url"; +import { dirname } from "path"; -export default [ - eslint.configs.recommended, - ...tseslint.configs.recommended, +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname +}); + +const eslintConfig = [ + { + ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts"], + }, + // Base JS/TS config { files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"], - plugins: { - "@next/next": nextPlugin, - "unicorn": unicornPlugin, - }, languageOptions: { - globals: { - ...globals.browser, - ...globals.node, - }, - parser: tseslint.parser, - parserOptions: { - project: "./tsconfig.json", - }, + globals: { + ...globals.browser, + ...globals.node, + }, + }, + plugins: { + "@next/next": nextPlugin, }, rules: { - ...nextPlugin.configs.recommended.rules, - ...nextPlugin.configs["core-web-vitals"].rules, - ...unicornPlugin.configs.recommended.rules, + ...nextPlugin.configs.recommended.rules, + ...nextPlugin.configs["core-web-vitals"].rules, } }, + // TypeScript specific config + { + files: ["**/*.{ts,tsx}"], + plugins: { + "unicorn": unicornPlugin, + }, + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: "./tsconfig.json", + }, + }, + rules: { + ...tseslint.configs.recommended.rules, + ...unicornPlugin.configs.recommended.rules, + // Disable noisy unicorn rules + "unicorn/prevent-abbreviations": "off", + "unicorn/filename-case": "off", + "unicorn/no-null": "off", + "unicorn/consistent-function-scoping": "off", + "unicorn/no-array-for-each": "off", + "unicorn/catch-error-name": "off", + "unicorn/explicit-length-check": "off", + "unicorn/no-array-reduce": "off", + "unicorn/prefer-spread": "off", + "unicorn/no-document-cookie": "off", + "unicorn/prefer-query-selector": "off", + "unicorn/prefer-add-event-listener": "off", + "unicorn/prefer-string-slice": "off", + "unicorn/prefer-string-replace-all": "off", + "unicorn/prefer-number-properties": "off", + "unicorn/consistent-existence-index-check": "off", + "unicorn/no-negated-condition": "off", + "unicorn/switch-case-braces": "off", + "unicorn/prefer-global-this": "off", + "unicorn/no-useless-undefined": "off", + "unicorn/no-array-callback-reference": "off", + "unicorn/no-array-sort": "off", + "unicorn/numeric-separators-style": "off", + "unicorn/prefer-optional-catch-binding": "off", + "unicorn/prefer-ternary": "off", + "unicorn/prefer-code-point": "off", + "unicorn/prefer-single-call": "off", + } + }, prettierConfig, -]; \ No newline at end of file + ...compat.extends("next/core-web-vitals"), +]; + +export default eslintConfig; diff --git a/susconecta/lib/auth.ts b/susconecta/lib/auth.ts index 1e0ea62..e87a2ad 100644 --- a/susconecta/lib/auth.ts +++ b/susconecta/lib/auth.ts @@ -103,6 +103,11 @@ export async function loginUser( payload, timestamp: new Date().toLocaleTimeString() }); + + console.log('🔑 [AUTH-API] Credenciais sendo usadas no login:'); + console.log('📧 Email:', email); + console.log('🔐 Senha:', password); + console.log('👤 UserType:', userType); // Delay para visualizar na aba Network await new Promise(resolve => setTimeout(resolve, 50)); @@ -113,53 +118,19 @@ export async function loginUser( // Debug: Log request sem credenciais sensíveis debugRequest('POST', url, getLoginHeaders(), payload); - let response = await fetch(url, { + const response = await fetch(url, { method: 'POST', headers: getLoginHeaders(), body: JSON.stringify(payload), }); - // Se login falhar com 400, tentar criar usuário automaticamente - if (!response.ok && response.status === 400) { - console.log('[AUTH-API] Login falhou (400), tentando criar usuário...'); - - const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`; - const signupPayload = { - email, - password, - data: { - userType: userType, - name: email.split('@')[0], - } - }; - - debugRequest('POST', signupUrl, getLoginHeaders(), signupPayload); - - const signupResponse = await fetch(signupUrl, { - method: 'POST', - headers: getLoginHeaders(), - body: JSON.stringify(signupPayload), - }); - - if (signupResponse.ok) { - console.log('[AUTH-API] Usuário criado, tentando login novamente...'); - await new Promise(resolve => setTimeout(resolve, 100)); - - response = await fetch(url, { - method: 'POST', - headers: getLoginHeaders(), - body: JSON.stringify(payload), - }); - } - } - console.log(`[AUTH-API] Login response: ${response.status} ${response.statusText}`, { url: response.url, status: response.status, timestamp: new Date().toLocaleTimeString() }); - // Se ainda for 400, mostrar detalhes do erro + // Se falhar, mostrar detalhes do erro if (!response.ok) { try { const errorText = await response.text(); diff --git a/susconecta/package.json b/susconecta/package.json index d0eacfd..6b9807d 100644 --- a/susconecta/package.json +++ b/susconecta/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "next build", "dev": "next dev", - "lint": "next lint", + "lint": "eslint .", "start": "next start" }, "dependencies": { @@ -86,6 +86,7 @@ "tailwindcss": "^4.1.9", "tw-animate-css": "1.3.3", "typescript": "^5", - "typescript-eslint": "^8.45.0" + "typescript-eslint": "^8.45.0", + "@eslint/eslintrc": "^3" } } diff --git a/susconecta/pnpm-lock.yaml b/susconecta/pnpm-lock.yaml index 6b1edf6..1758322 100644 --- a/susconecta/pnpm-lock.yaml +++ b/susconecta/pnpm-lock.yaml @@ -183,6 +183,9 @@ importers: specifier: 3.25.67 version: 3.25.67 devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.1 '@eslint/js': specifier: ^9.36.0 version: 9.36.0