From fb578b2a7a2a6dba94ac8a7266a75a38a9de169d Mon Sep 17 00:00:00 2001 From: M-Gabrielly Date: Tue, 14 Oct 2025 17:02:26 -0300 Subject: [PATCH] feat(api): add server-side /api/create-user route (user creation not functional yet) - What was done: - Added a server-side Next.js route at `src/app/api/create-user/route.ts` that validates the requester token, checks roles, generates a temporary password and forwards the creation to the Supabase Edge Function using the service role key. - Client wired to call the route via `lib/config.ts` (`FUNCTIONS_ENDPOINTS.CREATE_USER` -> `/api/create-user`) and the `criarUsuario()` wrapper in `lib/api.ts`. - Status / missing work: - Important: user creation is NOT working yet (requests to `/api/create-user` return 404 in dev). - Next steps: restart dev server, ensure `SUPABASE_SERVICE_ROLE_KEY` is set in the environment, check server logs and run a test POST with a valid admin JWT. --- susconecta/app/paciente/page.tsx | 15 +- susconecta/components/credentials-dialog.tsx | 57 +- .../forms/doctor-registration-form.tsx | 178 ++----- .../forms/patient-registration-form.tsx | 176 ++----- susconecta/eslint-report.json | 1 + susconecta/hooks/useAuth.tsx | 165 ++---- susconecta/lib/api.ts | 487 ++++-------------- susconecta/lib/auth.ts | 254 ++------- susconecta/lib/config.ts | 14 +- susconecta/lib/env-config.ts | 9 +- susconecta/src/app/api/create-user/route.ts | 172 +++++++ susconecta/src/app/api/link-paciente/route.ts | 80 +++ 12 files changed, 604 insertions(+), 1004 deletions(-) create mode 100644 susconecta/eslint-report.json create mode 100644 susconecta/src/app/api/create-user/route.ts create mode 100644 susconecta/src/app/api/link-paciente/route.ts diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index 8bd8e24..8bc6fed 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -693,8 +693,13 @@ export default function PacientePage() { } // Renderização principal + // Nota: removemos a restrição por `requiredUserType` aqui para permitir + // que qualquer usuário autenticado (qualquer role) acesse a área de paciente. + // A atribuição de roles e o vínculo entre Auth user e perfis é responsabilidade + // exclusiva da API server-side — o client NÃO deve escrever `user_id` em + // `patients` nem tentar atribuir roles. return ( - +
{/* Header só com título e botão de sair */}
@@ -723,6 +728,14 @@ export default function PacientePage() { {/* Conteúdo principal */} + {/* Banner informativo visível ao usuário/operador */} +
+
+ Observação: qualquer usuário autenticado pode acessar esta área do paciente. +
+ Importante: o perfil de paciente no backend não contém um campo `user_id` que o cliente deva manipular. A atribuição de papéis/roles e vinculação entre usuário de autenticação e perfil é feita somente pela API. +
+
{/* Toasts de feedback */} {toast && ( diff --git a/susconecta/components/credentials-dialog.tsx b/susconecta/components/credentials-dialog.tsx index 8f2401d..f4941fd 100644 --- a/susconecta/components/credentials-dialog.tsx +++ b/susconecta/components/credentials-dialog.tsx @@ -12,7 +12,7 @@ export interface CredentialsDialogProps { open: boolean; onOpenChange: (open: boolean) => void; email: string; - password: string; + password?: string | null; userName: string; userType: "médico" | "paciente"; } @@ -36,13 +36,14 @@ export function CredentialsDialog({ } function handleCopyPassword() { + if (!password) return; navigator.clipboard.writeText(password); setCopiedPassword(true); setTimeout(() => setCopiedPassword(false), 2000); } function handleCopyBoth() { - const text = `Email: ${email}\nSenha: ${password}`; + const text = password ? `Email: ${email}\nSenha: ${password}` : `Email: ${email}`; navigator.clipboard.writeText(text); } @@ -89,36 +90,40 @@ export function CredentialsDialog({
-
-
- + {password ? ( +
+
+ + +
- -
+ ) : ( +
Nenhuma senha foi retornada pela API. Verifique no painel administrativo ou gere uma nova senha.
+ )}
diff --git a/susconecta/components/forms/doctor-registration-form.tsx b/susconecta/components/forms/doctor-registration-form.tsx index e2655f1..f9d5cf1 100644 --- a/susconecta/components/forms/doctor-registration-form.tsx +++ b/susconecta/components/forms/doctor-registration-form.tsx @@ -24,13 +24,11 @@ import { removerAnexoMedico, MedicoInput, Medico, - criarUsuarioMedico, - gerarSenhaAleatoria, } from "@/lib/api"; ; +import { CredentialsDialog } from "@/components/credentials-dialog"; import { buscarCepAPI } from "@/lib/api"; -import { CredentialsDialog } from "@/components/credentials-dialog"; type FormacaoAcademica = { instituicao: string; @@ -150,18 +148,17 @@ export function DoctorRegistrationForm({ const [errors, setErrors] = useState>({}); const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false, formacao: false, admin: false }); const [isSubmitting, setSubmitting] = useState(false); + // credenciais geradas (após criação server-side) + const [credOpen, setCredOpen] = useState(false); + const [credEmail, setCredEmail] = useState(""); + const [credPassword, setCredPassword] = useState(""); + const [credUserName, setCredUserName] = useState(""); + const [credUserType, setCredUserType] = useState<"médico" | "paciente">("médico"); const [isSearchingCEP, setSearchingCEP] = useState(false); const [photoPreview, setPhotoPreview] = useState(null); const [serverAnexos, setServerAnexos] = useState([]); - // Estados para o dialog de credenciais - const [showCredentialsDialog, setShowCredentialsDialog] = useState(false); - const [credentials, setCredentials] = useState<{ - email: string; - password: string; - userName: string; - userType: 'médico' | 'paciente'; - } | null>(null); + // Não exibimos credenciais no cliente const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]); @@ -420,93 +417,49 @@ async function handleSubmit(ev: React.FormEvent) { const savedDoctorProfile = await criarMedico(medicoPayload); console.log("✅ Perfil do médico criado:", savedDoctorProfile); - // 2. Cria usuário no Supabase Auth (direto via /auth/v1/signup) - console.log('🔐 Criando usuário de autenticação...'); - - try { - const authResponse = await criarUsuarioMedico({ - email: form.email, - full_name: form.full_name, - phone_mobile: form.celular || '', - }); - - if (authResponse.success && authResponse.user) { - console.log('✅ Usuário Auth criado:', authResponse.user.id); - - // Attempt to link the created auth user id to the doctors record + // Nota: criação de credenciais é feita server-side. Chamamos a rota administrativa try { - // savedDoctorProfile may be an array or object depending on API - const docId = (savedDoctorProfile && (savedDoctorProfile.id || (Array.isArray(savedDoctorProfile) ? savedDoctorProfile[0]?.id : undefined))) || null; - if (docId) { - console.log('[DoctorForm] Vinculando user_id ao médico:', { doctorId: docId, userId: authResponse.user.id }); - // dynamic import to avoid circular deps in some bundlers - const api = await import('@/lib/api'); - if (api && typeof api.vincularUserIdMedico === 'function') { - await api.vincularUserIdMedico(String(docId), String(authResponse.user.id)); - console.log('[DoctorForm] user_id vinculado com sucesso.'); + const token = typeof window !== 'undefined' ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null; + const headers: Record = { 'Content-Type': 'application/json' }; + if (token) headers['Authorization'] = `Bearer ${token}`; + const createResp = await fetch('/api/create-user', { + method: 'POST', + headers, + body: JSON.stringify({ email: savedDoctorProfile.email, full_name: savedDoctorProfile.full_name, role: 'medico' }) + }); + + if (createResp.ok) { + const json = await createResp.json(); + // função server retorna objeto com user + possivelmente email/password + // alguns deployments retornam email/password em json.user or top-level + const email = json?.email ?? json?.user?.email ?? savedDoctorProfile.email; + const password = json?.password ?? json?.user?.password ?? json?.user?.temp_password ?? null; + + // Atualiza estados e abre dialog se recebido password + setCredEmail(email ?? savedDoctorProfile.email ?? ''); + setCredUserName(savedDoctorProfile.full_name ?? ''); + if (password) { + setCredPassword(String(password)); + setCredOpen(true); + } else { + // se não recebeu senha, apenas avisar que usuário foi criado + alert('Médico cadastrado com sucesso. A conta foi criada, mas a senha temporária não foi retornada pela API.'); } } else { - console.warn('[DoctorForm] Não foi possível determinar o ID do médico para vincular user_id. Doctor profile:', savedDoctorProfile); + const txt = await createResp.text().catch(() => ''); + console.warn('[doctor-form] create-user não ok:', createResp.status, txt); + alert('Médico cadastrado com sucesso. Falha ao criar credenciais automaticamente. Verifique os logs do servidor.'); } - } catch (linkErr) { - console.warn('[DoctorForm] Falha ao vincular user_id ao médico:', linkErr); + } catch (e) { + console.error('[doctor-form] erro ao criar credenciais server-side', e); + alert('Médico cadastrado com sucesso. Não foi possível criar credenciais automaticamente (erro de rede).'); } - // 3. Exibe popup com credenciais - setCredentials({ - email: authResponse.email, - password: authResponse.password, - userName: form.full_name, - userType: 'médico', - }); - setShowCredentialsDialog(true); - - // 4. Limpa formulário + // Limpa formulário setForm(initial); setPhotoPreview(null); setServerAnexos([]); - - // 5. Notifica componente pai onSaved?.(savedDoctorProfile); - } else { - throw new Error('Falha ao criar usuário de autenticação'); - } - - } catch (authError: any) { - console.error('❌ Erro ao criar usuário Auth:', authError); - - const errorMsg = authError?.message || String(authError); - - // Mensagens específicas de erro - if (errorMsg.toLowerCase().includes('already registered') || - errorMsg.toLowerCase().includes('already been registered') || - errorMsg.toLowerCase().includes('já está cadastrado')) { - alert( - `⚠️ EMAIL JÁ CADASTRADO\n\n` + - `O email "${form.email}" já possui uma conta no sistema.\n\n` + - `✅ O perfil do médico "${form.full_name}" foi salvo com sucesso.\n\n` + - `❌ Porém, não foi possível criar o login porque este email já está em uso.\n\n` + - `SOLUÇÃO:\n` + - `• Use um email diferente para este médico, OU\n` + - `• Se o médico já tem conta, edite o perfil e vincule ao usuário existente` - ); - } else { - alert( - `⚠️ Médico cadastrado com sucesso, mas houve um problema ao criar o acesso ao sistema.\n\n` + - `✅ Perfil do médico salvo: ${form.full_name}\n\n` + - `❌ Erro ao criar login: ${errorMsg}\n\n` + - `Por favor, entre em contato com o administrador para criar o acesso manualmente.` - ); - } - - // Limpa formulário mesmo com erro - setForm(initial); - setPhotoPreview(null); - setServerAnexos([]); - onSaved?.(savedDoctorProfile); - if (inline) onClose?.(); - else onOpenChange?.(false); - } } } catch (err: any) { console.error("❌ Erro no handleSubmit:", err); @@ -1009,28 +962,7 @@ async function handleSubmit(ev: React.FormEvent) { <>
{content}
- {/* Dialog de credenciais */} - {credentials && ( - { - setShowCredentialsDialog(open); - if (!open) { - // Quando o dialog de credenciais fecha, fecha o formulário também - setCredentials(null); - if (inline) { - onClose?.(); - } else { - onOpenChange?.(false); - } - } - }} - email={credentials.email} - password={credentials.password} - userName={credentials.userName} - userType={credentials.userType} - /> - )} + {/* Credenciais não exibidas no cliente */} ); } @@ -1048,23 +980,15 @@ async function handleSubmit(ev: React.FormEvent) { - {/* Dialog de credenciais */} - {credentials && ( - { - setShowCredentialsDialog(open); - if (!open) { - setCredentials(null); - onOpenChange?.(false); - } - }} - email={credentials.email} - password={credentials.password} - userName={credentials.userName} - userType={credentials.userType} - /> - )} + {/* Exibe credenciais retornadas pelo servidor (quando houver) */} + setCredOpen(v)} + email={credEmail} + password={credPassword} + userName={credUserName} + userType={credUserType} + /> ); } diff --git a/susconecta/components/forms/patient-registration-form.tsx b/susconecta/components/forms/patient-registration-form.tsx index 6ca311b..26be5b2 100644 --- a/susconecta/components/forms/patient-registration-form.tsx +++ b/susconecta/components/forms/patient-registration-form.tsx @@ -25,13 +25,12 @@ import { listarAnexos, removerAnexo, buscarPacientePorId, - criarUsuarioPaciente, criarPaciente, } from "@/lib/api"; +import { CredentialsDialog } from "@/components/credentials-dialog"; import { validarCPFLocal } from "@/lib/utils"; import { verificarCpfDuplicado } from "@/lib/api"; -import { CredentialsDialog } from "@/components/credentials-dialog"; type Mode = "create" | "edit"; @@ -100,18 +99,17 @@ export function PatientRegistrationForm({ const [errors, setErrors] = useState>({}); const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false }); const [isSubmitting, setSubmitting] = useState(false); + // credenciais geradas server-side + const [credOpen, setCredOpen] = useState(false); + const [credEmail, setCredEmail] = useState(""); + const [credPassword, setCredPassword] = useState(""); + const [credUserName, setCredUserName] = useState(""); + const [credUserType, setCredUserType] = useState<"médico" | "paciente">("paciente"); const [isSearchingCEP, setSearchingCEP] = useState(false); const [photoPreview, setPhotoPreview] = useState(null); const [serverAnexos, setServerAnexos] = useState([]); - // Estados para o dialog de credenciais - const [showCredentialsDialog, setShowCredentialsDialog] = useState(false); - const [credentials, setCredentials] = useState<{ - email: string; - password: string; - userName: string; - userType: 'médico' | 'paciente'; - } | null>(null); + // Credenciais/usuário não são criados no cliente const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]); @@ -304,94 +302,45 @@ export function PatientRegistrationForm({ const savedPatientProfile = await criarPaciente(patientPayload); console.log(" Perfil do paciente criado:", savedPatientProfile); - if (form.email && form.email.includes('@')) { - console.log(" Criando usuário de autenticação (paciente)..."); + // Depois de criar perfil, solicitar criação de credenciais server-side try { - const userResponse = await criarUsuarioPaciente({ - email: form.email, - full_name: form.nome, - phone_mobile: form.telefone, + const token = typeof window !== 'undefined' ? (localStorage.getItem('auth_token') || localStorage.getItem('token') || sessionStorage.getItem('auth_token') || sessionStorage.getItem('token')) : null; + const headers: Record = { 'Content-Type': 'application/json' }; + if (token) headers['Authorization'] = `Bearer ${token}`; + const resp = await fetch('/api/create-user', { + method: 'POST', + headers, + body: JSON.stringify({ email: savedPatientProfile.email, full_name: savedPatientProfile.full_name, role: 'user' }) }); - if (userResponse.success && userResponse.user) { - console.log(" Usuário de autenticação criado:", userResponse.user); - - // Mostra credenciais no dialog usando as credenciais retornadas - setCredentials({ - email: userResponse.email ?? form.email, - password: userResponse.password ?? '', - userName: form.nome, - userType: 'paciente', - }); - setShowCredentialsDialog(true); - - // Tenta vincular o user_id ao perfil do paciente recém-criado - try { - const apiMod = await import('@/lib/api'); - const pacienteId = savedPatientProfile?.id || (savedPatientProfile && (savedPatientProfile as any).id); - const userId = (userResponse.user as any)?.id || (userResponse.user as any)?.user_id || (userResponse.user as any)?.id; - if (pacienteId && userId && typeof apiMod.vincularUserIdPaciente === 'function') { - console.log('[PatientForm] Vinculando user_id ao paciente:', pacienteId, userId); - try { - await apiMod.vincularUserIdPaciente(pacienteId, String(userId)); - console.log('[PatientForm] user_id vinculado com sucesso ao paciente'); - } catch (linkErr) { - console.warn('[PatientForm] Falha ao vincular user_id ao paciente:', linkErr); - } - } - } catch (dynErr) { - console.warn('[PatientForm] Não foi possível importar helper para vincular user_id:', dynErr); + if (resp.ok) { + const json = await resp.json(); + const email = json?.email ?? json?.user?.email ?? savedPatientProfile.email; + const password = json?.password ?? json?.user?.password ?? json?.user?.temp_password ?? null; + setCredEmail(email ?? savedPatientProfile.email ?? ''); + setCredUserName(savedPatientProfile.full_name ?? ''); + if (password) { + setCredPassword(String(password)); + setCredOpen(true); + } else { + alert('Paciente cadastrado com sucesso. Conta criada, mas a senha temporária não foi retornada.'); } - - // Limpa formulário mas NÃO fecha ainda - fechará quando o dialog de credenciais fechar - setForm(initial); - setPhotoPreview(null); - setServerAnexos([]); - onSaved?.(savedPatientProfile); - return; } else { - throw new Error((userResponse as any).message || "Falhou ao criar o usuário de acesso."); + const txt = await resp.text().catch(() => ''); + console.warn('[patient-form] create-user failed', resp.status, txt); + alert('Paciente cadastrado com sucesso. Falha ao criar credenciais automaticamente.'); } - } catch (userError: any) { - console.error(" Erro ao criar usuário via signup:", userError); - - // Mensagem de erro específica para email duplicado - const errorMsg = userError?.message || String(userError); - - if (errorMsg.toLowerCase().includes('already registered') || - errorMsg.toLowerCase().includes('já está cadastrado') || - errorMsg.toLowerCase().includes('já existe')) { - alert( - ` Este email já está cadastrado no sistema.\n\n` + - ` O perfil do paciente foi salvo com sucesso.\n\n` + - `Para criar acesso ao sistema, use um email diferente ou recupere a senha do email existente.` - ); - } else { - alert( - ` Paciente cadastrado com sucesso!\n\n` + - ` Porém houve um problema ao criar o acesso:\n${errorMsg}\n\n` + - `O cadastro do paciente foi salvo, mas será necessário criar o acesso manualmente.` - ); - } - - // Limpa formulário e fecha - setForm(initial); - setPhotoPreview(null); - setServerAnexos([]); - onSaved?.(savedPatientProfile); - if (inline) onClose?.(); - else onOpenChange?.(false); - return; + } catch (e) { + console.error('[patient-form] erro ao criar credenciais server-side', e); + alert('Paciente cadastrado com sucesso. Não foi possível criar credenciais automaticamente (erro de rede).'); } - } else { - alert("Paciente cadastrado com sucesso (sem usuário de acesso - email não fornecido)."); + onSaved?.(savedPatientProfile); setForm(initial); setPhotoPreview(null); setServerAnexos([]); if (inline) onClose?.(); else onOpenChange?.(false); - } } } catch (err: any) { console.error("❌ Erro no handleSubmit:", err); @@ -747,28 +696,15 @@ export function PatientRegistrationForm({ <>
{content}
- {/* Dialog de credenciais */} - {credentials && ( - { - setShowCredentialsDialog(open); - if (!open) { - // Quando o dialog de credenciais fecha, fecha o formulário também - setCredentials(null); - if (inline) { - onClose?.(); - } else { - onOpenChange?.(false); - } - } - }} - email={credentials.email} - password={credentials.password} - userName={credentials.userName} - userType={credentials.userType} - /> - )} + {/* Credenciais não exibidas no cliente */} + setCredOpen(v)} + email={credEmail} + password={credPassword} + userName={credUserName} + userType={credUserType} + /> ); } @@ -786,23 +722,15 @@ export function PatientRegistrationForm({ - {/* Dialog de credenciais */} - {credentials && ( - { - setShowCredentialsDialog(open); - if (!open) { - setCredentials(null); - onOpenChange?.(false); - } - }} - email={credentials.email} - password={credentials.password} - userName={credentials.userName} - userType={credentials.userType} - /> - )} + {/* Credenciais não exibidas no cliente */} + setCredOpen(v)} + email={credEmail} + password={credPassword} + userName={credUserName} + userType={credUserType} + /> ); } \ No newline at end of file diff --git a/susconecta/eslint-report.json b/susconecta/eslint-report.json new file mode 100644 index 0000000..c266f1b --- /dev/null +++ b/susconecta/eslint-report.json @@ -0,0 +1 @@ +[{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\(main-routes)\\calendar\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\(main-routes)\\consultas\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\(main-routes)\\dashboard\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\(main-routes)\\dashboard\\relatorios\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\(main-routes)\\doutores\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\(main-routes)\\layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\(main-routes)\\pacientes\\loading.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\(main-routes)\\pacientes\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\agenda\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\financeiro\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\layout.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\login-admin\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\login-paciente\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\login\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\paciente\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\procedimento\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\profissional\\page.tsx","messages":[{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useEffect has a missing dependency: 'user'. Either include it or remove the dependency array.","line":167,"column":6,"nodeType":"ArrayExpression","endLine":167,"endColumn":26,"suggestions":[{"desc":"Update the dependencies array to be: [user.id, doctorId, user]","fix":{"range":[6724,6744],"text":"[user.id, doctorId, user]"}}]},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useEffect has a missing dependency: 'user'. Either include it or remove the dependency array.","line":219,"column":6,"nodeType":"ArrayExpression","endLine":219,"endColumn":29,"suggestions":[{"desc":"Update the dependencies array to be: [user.id, user.email, user]","fix":{"range":[9697,9720],"text":"[user.id, user.email, user]"}}]},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useEffect has a missing dependency: 'reports'. Either include it or remove the dependency array. Outer scope values like 'user.id' aren't valid dependencies because mutating them doesn't re-render the component.","line":988,"column":8,"nodeType":"ArrayExpression","endLine":988,"endColumn":18,"suggestions":[{"desc":"Update the dependencies array to be: [reports]","fix":{"range":[40874,40884],"text":"[reports]"}}]},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useEffect has a missing dependency: 'laudos'. Either include it or remove the dependency array.","line":993,"column":8,"nodeType":"ArrayExpression","endLine":993,"endColumn":17,"suggestions":[{"desc":"Update the dependencies array to be: [laudos, reports]","fix":{"range":[41050,41059],"text":"[laudos, reports]"}}]},{"ruleId":"unicorn/prefer-module","severity":2,"message":"Do not use \"require\".","line":1457,"column":21,"nodeType":"Identifier","messageId":"error/identifier","endLine":1457,"endColumn":28},{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useEffect has an unnecessary dependency: 'user'. Either exclude it or remove the dependency array. Outer scope values like 'user' aren't valid dependencies because mutating them doesn't re-render the component.","line":1606,"column":8,"nodeType":"ArrayExpression","endLine":1606,"endColumn":70,"suggestions":[{"desc":"Update the dependencies array to be: [laudo, isNewLaudo, pacienteSelecionado, listaPacientes]","fix":{"range":[71698,71760],"text":"[laudo, isNewLaudo, pacienteSelecionado, listaPacientes]"}}]},{"ruleId":"@next/next/no-img-element","severity":1,"message":"Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element","line":2038,"column":27,"nodeType":"JSXOpeningElement","endLine":2042,"endColumn":29},{"ruleId":"@next/next/no-img-element","severity":1,"message":"Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element","line":2205,"column":29,"nodeType":"JSXOpeningElement","endLine":2210,"endColumn":31},{"ruleId":"@next/next/no-img-element","severity":1,"message":"Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element","line":2220,"column":29,"nodeType":"JSXOpeningElement","endLine":2220,"endColumn":126},{"ruleId":"unicorn/no-lonely-if","severity":2,"message":"Unexpected `if` as the only statement in a `if` block without `else`.","line":2316,"column":31,"nodeType":"IfStatement","messageId":"no-lonely-if","endLine":2316,"endColumn":98,"fix":{"range":[109490,109657],"text":"(val !== undefined && val !== null && JSON.stringify(origVal) !== JSON.stringify(val)) diff[k] = val;"}},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`\"` can be escaped with `"`, `“`, `"`, `”`.","line":2414,"column":36,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[114977,115006],"text":""Ok, obrigado pelo lembrete!\""},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"“"},"fix":{"range":[114977,115006],"text":"“Ok, obrigado pelo lembrete!\""},"desc":"Replace with `“`."},{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[114977,115006],"text":""Ok, obrigado pelo lembrete!\""},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"”"},"fix":{"range":[114977,115006],"text":"”Ok, obrigado pelo lembrete!\""},"desc":"Replace with `”`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`\"` can be escaped with `"`, `“`, `"`, `”`.","line":2414,"column":64,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[114977,115006],"text":"\"Ok, obrigado pelo lembrete!""},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"“"},"fix":{"range":[114977,115006],"text":"\"Ok, obrigado pelo lembrete!“"},"desc":"Replace with `“`."},{"messageId":"replaceWithAlt","data":{"alt":"""},"fix":{"range":[114977,115006],"text":"\"Ok, obrigado pelo lembrete!""},"desc":"Replace with `"`."},{"messageId":"replaceWithAlt","data":{"alt":"”"},"fix":{"range":[114977,115006],"text":"\"Ok, obrigado pelo lembrete!”"},"desc":"Replace with `”`."}]}],"suppressedMessages":[{"ruleId":"react-hooks/exhaustive-deps","severity":1,"message":"React Hook useEffect has missing dependencies: 'history' and 'historyIndex'. Either include them or remove the dependency array.","line":1620,"column":8,"nodeType":"ArrayExpression","endLine":1620,"endColumn":17,"suggestions":[{"desc":"Update the dependencies array to be: [content, history, historyIndex]","fix":{"range":[72242,72251],"text":"[content, history, historyIndex]"}}],"suppressions":[{"kind":"directive","justification":""}]}],"errorCount":4,"fatalErrorCount":0,"warningCount":8,"fixableErrorCount":1,"fixableWarningCount":0,"source":"\"use client\";\r\nimport React, { useState, useRef, useEffect } from \"react\";\r\nimport SignatureCanvas from \"react-signature-canvas\";\r\nimport Link from \"next/link\";\r\nimport ProtectedRoute from \"@/components/ProtectedRoute\";\r\nimport { useAuth } from \"@/hooks/useAuth\";\r\nimport { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, type Paciente, buscarRelatorioPorId, atualizarMedico } from \"@/lib/api\";\r\nimport { useReports } from \"@/hooks/useReports\";\r\nimport { CreateReportData } from \"@/types/report-types\";\r\nimport { Button } from \"@/components/ui/button\";\r\nimport { Input } from \"@/components/ui/input\";\r\nimport { Label } from \"@/components/ui/label\";\r\nimport { Textarea } from \"@/components/ui/textarea\";\r\nimport { SimpleThemeToggle } from \"@/components/simple-theme-toggle\";\r\nimport {\r\n Table,\r\n TableBody,\r\n TableCell,\r\n TableHead,\r\n TableHeader,\r\n TableRow,\r\n} from \"@/components/ui/table\";\r\nimport {\r\n Select,\r\n SelectContent,\r\n SelectItem,\r\n SelectTrigger,\r\n SelectValue,\r\n} from \"@/components/ui/select\";\r\nimport { Avatar, AvatarImage, AvatarFallback } from \"@/components/ui/avatar\"\r\nimport { User, FolderOpen, X, Users, MessageSquare, ClipboardList, Plus, Edit, Trash2, ChevronLeft, ChevronRight, Clock, FileCheck, Upload, Download, Eye, History, Stethoscope, Pill, Activity, Search } from \"lucide-react\"\r\nimport { Calendar as CalendarIcon, FileText, Settings } from \"lucide-react\";\r\nimport {\r\n Tooltip,\r\n TooltipContent,\r\n TooltipProvider,\r\n TooltipTrigger,\r\n} from \"@/components/ui/tooltip\";\r\n\r\n\r\nimport dynamic from \"next/dynamic\";\r\nimport dayGridPlugin from \"@fullcalendar/daygrid\";\r\nimport timeGridPlugin from \"@fullcalendar/timegrid\";\r\nimport interactionPlugin from \"@fullcalendar/interaction\";\r\nimport ptBrLocale from \"@fullcalendar/core/locales/pt-br\";\r\n\r\nconst FullCalendar = dynamic(() => import(\"@fullcalendar/react\"), {\r\n ssr: false,\r\n});\r\n\r\n// pacientes will be loaded inside the component (hooks must run in component body)\r\n\r\n// removed static medico placeholder; will load real profile for logged-in user\r\n\r\n\r\nconst colorsByType = {\r\n Rotina: \"#4dabf7\",\r\n Cardiologia: \"#f76c6c\",\r\n Otorrino: \"#f7b84d\",\r\n Pediatria: \"#6cf78b\",\r\n Dermatologia: \"#9b59b6\",\r\n Oftalmologia: \"#2ecc71\"\r\n};\r\n\r\n // Helpers para normalizar dados de paciente (suporta schema antigo e novo)\r\n const getPatientName = (p: any) => p?.full_name ?? p?.nome ?? '';\r\n const getPatientCpf = (p: any) => p?.cpf ?? '';\r\n const getPatientSex = (p: any) => p?.sex ?? p?.sexo ?? '';\r\n const getPatientId = (p: any) => p?.id ?? '';\r\n const getPatientAge = (p: any) => {\r\n if (!p) return '';\r\n // Prefer birth_date (ISO) to calcular idade\r\n const bd = p?.birth_date ?? p?.data_nascimento ?? p?.birthDate;\r\n if (bd) {\r\n const d = new Date(bd);\r\n if (!isNaN(d.getTime())) {\r\n const age = Math.floor((Date.now() - d.getTime()) / (1000 * 60 * 60 * 24 * 365.25));\r\n return `${age}`;\r\n }\r\n }\r\n // Fallback para campo idade/idade_anterior\r\n return p?.idade ?? p?.age ?? '';\r\n };\r\n\r\n // Helpers para normalizar campos do laudo/relatório\r\n const getReportPatientName = (r: any) => r?.paciente?.full_name ?? r?.paciente?.nome ?? r?.patient?.full_name ?? r?.patient?.nome ?? r?.patient_name ?? r?.patient_full_name ?? '';\r\n const getReportPatientId = (r: any) => r?.paciente?.id ?? r?.patient?.id ?? r?.patient_id ?? r?.patientId ?? r?.patient_id_raw ?? r?.patient_id ?? r?.id ?? '';\r\n const getReportPatientCpf = (r: any) => r?.paciente?.cpf ?? r?.patient?.cpf ?? r?.patient_cpf ?? '';\r\n const getReportExecutor = (r: any) => r?.executante ?? r?.requested_by ?? r?.requestedBy ?? r?.created_by ?? r?.createdBy ?? r?.requested_by_name ?? r?.executor ?? '';\r\n const getReportExam = (r: any) => r?.exame ?? r?.exam ?? r?.especialidade ?? r?.cid_code ?? r?.report_type ?? '-';\r\n const getReportDate = (r: any) => r?.data ?? r?.created_at ?? r?.due_at ?? r?.report_date ?? '';\r\n const formatReportDate = (raw?: string) => {\r\n if (!raw) return '-';\r\n try {\r\n const d = new Date(raw);\r\n if (isNaN(d.getTime())) return raw;\r\n return d.toLocaleDateString('pt-BR');\r\n } catch (e) {\r\n return raw;\r\n }\r\n };\r\n\r\nconst ProfissionalPage = () => {\r\n const { logout, user } = useAuth();\r\n const [activeSection, setActiveSection] = useState('calendario');\r\n const [pacienteSelecionado, setPacienteSelecionado] = useState(null);\r\n \r\n // Estados para edição de laudo\r\n const [isEditingLaudoForPatient, setIsEditingLaudoForPatient] = useState(false);\r\n const [patientForLaudo, setPatientForLaudo] = useState(null);\r\n \r\n // Estados para o perfil do médico\r\n const [isEditingProfile, setIsEditingProfile] = useState(false);\r\n const [doctorId, setDoctorId] = useState(null);\r\n // Removemos o placeholder extenso — inicializamos com valores minimalistas e vazios.\r\n const [profileData, setProfileData] = useState({\r\n nome: '',\r\n email: user?.email || '',\r\n telefone: '',\r\n endereco: '',\r\n cidade: '',\r\n cep: '',\r\n crm: '',\r\n especialidade: '',\r\n // biografia field removed — not present in Medico records\r\n fotoUrl: ''\r\n });\r\n\r\n // pacientes carregados dinamicamente (hooks devem ficar dentro do componente)\r\n const [pacientes, setPacientes] = useState([]);\r\n useEffect(() => {\r\n let mounted = true;\r\n (async () => {\r\n try {\r\n if (!user || !user.id) {\r\n if (mounted) setPacientes([]);\r\n return;\r\n }\r\n\r\n const assignmentsMod = await import('@/lib/assignment');\r\n if (!assignmentsMod || typeof assignmentsMod.listAssignmentsForUser !== 'function') {\r\n if (mounted) setPacientes([]);\r\n return;\r\n }\r\n\r\n const assignments = await assignmentsMod.listAssignmentsForUser(user.id || '');\r\n const patientIds = Array.isArray(assignments) ? assignments.map((a:any) => String(a.patient_id)).filter(Boolean) : [];\r\n if (!patientIds.length) {\r\n if (mounted) setPacientes([]);\r\n return;\r\n }\r\n\r\n const patients = await buscarPacientesPorIds(patientIds);\r\n const normalized = (patients || []).map((p: any) => ({\r\n ...p,\r\n nome: p.full_name ?? (p as any).nome ?? '',\r\n cpf: p.cpf ?? '',\r\n idade: getPatientAge(p) // preencher idade para a tabela de pacientes\r\n }));\r\n if (mounted) setPacientes(normalized);\r\n } catch (err) {\r\n console.warn('[ProfissionalPage] falha ao carregar pacientes atribuídos:', err);\r\n if (mounted) setPacientes([]);\r\n }\r\n })();\r\n return () => { mounted = false; };\r\n }, [user?.id, doctorId]);\r\n\r\n // Carregar perfil do médico correspondente ao usuário logado\r\n useEffect(() => {\r\n let mounted = true;\r\n (async () => {\r\n try {\r\n if (!user || !user.email) return;\r\n // Tenta buscar médicos pelo email do usuário (buscarMedicos lida com queries por email)\r\n const docs = await buscarMedicos(user.email);\r\n if (!mounted) return;\r\n if (Array.isArray(docs) && docs.length > 0) {\r\n // preferir registro cujo user_id bate com user.id\r\n let chosen = docs.find(d => String((d as any).user_id) === String(user.id)) || docs[0];\r\n if (chosen) {\r\n // store the doctor's id so we can update it later\r\n try { setDoctorId((chosen as any).id ?? null); } catch {};\r\n // Especialidade pode vir como 'specialty' (inglês), 'especialidade' (pt),\r\n // ou até uma lista/array. Normalizamos para string.\r\n const rawSpecialty = (chosen as any).specialty ?? (chosen as any).especialidade ?? (chosen as any).especialidades ?? (chosen as any).especiality;\r\n let specialtyStr = '';\r\n if (Array.isArray(rawSpecialty)) {\r\n specialtyStr = rawSpecialty.join(', ');\r\n } else if (rawSpecialty) {\r\n specialtyStr = String(rawSpecialty);\r\n }\r\n\r\n // Foto pode vir como 'foto_url' ou 'fotoUrl' ou 'avatar_url'\r\n const foto = (chosen as any).foto_url || (chosen as any).fotoUrl || (chosen as any).avatar_url || '';\r\n\r\n setProfileData((prev) => ({\r\n ...prev,\r\n nome: (chosen as any).full_name || (chosen as any).nome_social || prev.nome || user?.email?.split('@')[0] || '',\r\n email: (chosen as any).email || user?.email || prev.email,\r\n telefone: (chosen as any).phone_mobile || (chosen as any).celular || (chosen as any).telefone || (chosen as any).phone || (chosen as any).mobile || (user as any)?.user_metadata?.phone || prev.telefone,\r\n endereco: (chosen as any).street || (chosen as any).endereco || prev.endereco,\r\n cidade: (chosen as any).city || (chosen as any).cidade || prev.cidade,\r\n cep: (chosen as any).cep || prev.cep,\r\n // store raw CRM (only the number) to avoid double-prefixing when rendering\r\n crm: (chosen as any).crm ? String((chosen as any).crm).replace(/^(?:CRM\\s*)+/i, '').trim() : (prev.crm || ''),\r\n especialidade: specialtyStr || prev.especialidade || '',\r\n // biografia removed: prefer to ignore observacoes/curriculo_url here\r\n // (if needed elsewhere, render directly from chosen.observacoes)\r\n fotoUrl: foto || prev.fotoUrl || ''\r\n }));\r\n }\r\n }\r\n } catch (e) {\r\n console.warn('[ProfissionalPage] falha ao carregar perfil do médico pelo email:', e);\r\n }\r\n })();\r\n return () => { mounted = false; };\r\n }, [user?.id, user?.email]);\r\n\r\n\r\n\r\n // Estados para campos principais da consulta\r\n const [consultaAtual, setConsultaAtual] = useState({\r\n patient_id: \"\",\r\n order_number: \"\",\r\n exam: \"\",\r\n diagnosis: \"\",\r\n conclusion: \"\",\r\n cid_code: \"\",\r\n content_html: \"\",\r\n content_json: {},\r\n status: \"draft\",\r\n requested_by: \"\",\r\n due_at: new Date().toISOString(),\r\n hide_date: true,\r\n hide_signature: true\r\n });\r\n\r\n \r\n \r\n const [events, setEvents] = useState([\r\n \r\n {\r\n id: 1,\r\n title: \"Ana Souza\",\r\n type: \"Cardiologia\",\r\n time: \"09:00\",\r\n date: new Date().toISOString().split('T')[0], \r\n pacienteId: \"123.456.789-00\",\r\n color: colorsByType.Cardiologia\r\n },\r\n {\r\n id: 2,\r\n title: \"Bruno Lima\",\r\n type: \"Cardiologia\",\r\n time: \"10:30\",\r\n date: new Date().toISOString().split('T')[0], \r\n pacienteId: \"987.654.321-00\",\r\n color: colorsByType.Cardiologia\r\n },\r\n {\r\n id: 3,\r\n title: \"Carla Menezes\",\r\n type: \"Dermatologia\",\r\n time: \"14:00\",\r\n date: new Date().toISOString().split('T')[0], \r\n pacienteId: \"111.222.333-44\",\r\n color: colorsByType.Dermatologia\r\n }\r\n ]);\r\n const [editingEvent, setEditingEvent] = useState(null);\r\n const [showPopup, setShowPopup] = useState(false);\r\n const [showActionModal, setShowActionModal] = useState(false);\r\n const [step, setStep] = useState(1);\r\n const [newEvent, setNewEvent] = useState({ \r\n title: \"\", \r\n type: \"\", \r\n time: \"\",\r\n pacienteId: \"\" \r\n });\r\n const [selectedDate, setSelectedDate] = useState(null);\r\n const [selectedEvent, setSelectedEvent] = useState(null);\r\n const [currentCalendarDate, setCurrentCalendarDate] = useState(new Date());\r\n\r\n const handleSave = (event: React.MouseEvent) => {\r\n event.preventDefault();\r\n console.log(\"Laudo salvo!\");\r\n window.scrollTo({ top: 0, behavior: \"smooth\" });\r\n };\r\n\r\n \r\n\r\n const handleEditarLaudo = (paciente: any) => {\r\n setPatientForLaudo(paciente);\r\n setIsEditingLaudoForPatient(true);\r\n setActiveSection('laudos');\r\n };\r\n\r\n \r\n const navigateDate = (direction: 'prev' | 'next') => {\r\n const newDate = new Date(currentCalendarDate);\r\n newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));\r\n setCurrentCalendarDate(newDate);\r\n };\r\n\r\n const goToToday = () => {\r\n setCurrentCalendarDate(new Date());\r\n };\r\n\r\n const formatDate = (date: Date) => {\r\n return date.toLocaleDateString('pt-BR', { \r\n weekday: 'long', \r\n day: 'numeric', \r\n month: 'long', \r\n year: 'numeric' \r\n });\r\n };\r\n\r\n // Filtrar eventos do dia atual\r\n const getTodayEvents = () => {\r\n const today = currentCalendarDate.toISOString().split('T')[0];\r\n return events\r\n .filter(event => event.date === today)\r\n .sort((a, b) => a.time.localeCompare(b.time));\r\n };\r\n\r\n const getStatusColor = (type: string) => {\r\n return colorsByType[type as keyof typeof colorsByType] || \"#4dabf7\";\r\n };\r\n\r\n // Funções para o perfil\r\n const handleProfileChange = (field: string, value: string) => {\r\n setProfileData(prev => ({\r\n ...prev,\r\n [field]: value\r\n }));\r\n };\r\n\r\n const handleSaveProfile = () => {\r\n (async () => {\r\n if (!doctorId) {\r\n alert('Não foi possível localizar o registro do médico para atualizar.');\r\n setIsEditingProfile(false);\r\n return;\r\n }\r\n\r\n // Build payload mapping UI fields to DB columns\r\n const payload: any = {};\r\n if (profileData.email) payload.email = profileData.email;\r\n if (profileData.telefone) payload.phone_mobile = profileData.telefone;\r\n if (profileData.endereco) payload.street = profileData.endereco;\r\n if (profileData.cidade) payload.city = profileData.cidade;\r\n if (profileData.cep) payload.cep = profileData.cep;\r\n if (profileData.especialidade) payload.specialty = profileData.especialidade || profileData.especialidade;\r\n if (profileData.fotoUrl) payload.foto_url = profileData.fotoUrl;\r\n\r\n // Don't allow updating full_name or crm from this UI\r\n\r\n try {\r\n const updated = await atualizarMedico(doctorId, payload as any);\r\n console.debug('[ProfissionalPage] médico atualizado:', updated);\r\n alert('Perfil atualizado com sucesso!');\r\n } catch (err: any) {\r\n console.error('[ProfissionalPage] falha ao atualizar médico:', err);\r\n // Mostrar mensagem amigável (o erro já é tratado em lib/api)\r\n alert(err?.message || 'Falha ao atualizar perfil. Verifique logs.');\r\n } finally {\r\n setIsEditingProfile(false);\r\n }\r\n })();\r\n };\r\n\r\n const handleCancelEdit = () => {\r\n setIsEditingProfile(false);\r\n };\r\n\r\n\r\n\r\n \r\n const handleDateClick = (arg: any) => {\r\n setSelectedDate(arg.dateStr);\r\n setNewEvent({ title: \"\", type: \"\", time: \"\", pacienteId: \"\" });\r\n setStep(1);\r\n setEditingEvent(null);\r\n setShowPopup(true);\r\n };\r\n\r\n \r\n const handleAddEvent = () => {\r\n const paciente = pacientes.find(p => p.nome === newEvent.title);\r\n const eventToAdd = {\r\n id: Date.now(),\r\n title: newEvent.title,\r\n type: newEvent.type,\r\n time: newEvent.time,\r\n date: selectedDate || currentCalendarDate.toISOString().split('T')[0],\r\n pacienteId: paciente ? paciente.cpf : \"\",\r\n color: colorsByType[newEvent.type as keyof typeof colorsByType] || \"#4dabf7\"\r\n };\r\n setEvents((prev) => [...prev, eventToAdd]);\r\n setShowPopup(false);\r\n };\r\n\r\n\r\n const handleEditEvent = () => {\r\n setEvents((prevEvents) =>\r\n prevEvents.map((ev) =>\r\n ev.id.toString() === editingEvent.id.toString()\r\n ? {\r\n ...ev,\r\n title: newEvent.title,\r\n type: newEvent.type,\r\n time: newEvent.time,\r\n color: colorsByType[newEvent.type as keyof typeof colorsByType] || \"#4dabf7\"\r\n }\r\n : ev\r\n )\r\n );\r\n setEditingEvent(null);\r\n setShowPopup(false);\r\n setShowActionModal(false);\r\n };\r\n\r\n \r\n const handleNextStep = () => {\r\n if (step < 3) setStep(step + 1);\r\n else editingEvent ? handleEditEvent() : handleAddEvent();\r\n };\r\n\r\n \r\n const handleEventClick = (clickInfo: any) => {\r\n setSelectedEvent(clickInfo.event);\r\n setShowActionModal(true);\r\n };\r\n\r\n\r\n const handleDeleteEvent = () => {\r\n if (!selectedEvent) return;\r\n setEvents((prevEvents) =>\r\n prevEvents.filter((ev: any) => ev.id.toString() !== selectedEvent.id.toString())\r\n );\r\n setShowActionModal(false);\r\n };\r\n\r\n \r\n const handleStartEdit = () => {\r\n if (!selectedEvent) return;\r\n setEditingEvent(selectedEvent);\r\n setNewEvent({\r\n title: selectedEvent.title,\r\n type: selectedEvent.extendedProps.type,\r\n time: selectedEvent.extendedProps.time,\r\n pacienteId: selectedEvent.extendedProps.pacienteId || \"\"\r\n });\r\n setStep(1);\r\n setShowActionModal(false);\r\n setShowPopup(true);\r\n };\r\n\r\n \r\n const renderEventContent = (eventInfo: any) => {\r\n const bg = eventInfo.event.backgroundColor || eventInfo.event.extendedProps?.color || \"#4dabf7\";\r\n\r\n return (\r\n \r\n {eventInfo.event.title}\r\n \r\n {eventInfo.event.extendedProps.type}\r\n \r\n {eventInfo.event.extendedProps.time}\r\n
\r\n );\r\n };\r\n\r\n \r\n const renderCalendarioSection = () => {\r\n const todayEvents = getTodayEvents();\r\n \r\n return (\r\n
\r\n
\r\n

Agenda do Dia

\r\n
\r\n \r\n {/* Navegação de Data */}\r\n
\r\n
\r\n \r\n

\r\n {formatDate(currentCalendarDate)}\r\n

\r\n \r\n \r\n
\r\n
\r\n {todayEvents.length} consulta{todayEvents.length !== 1 ? 's' : ''} agendada{todayEvents.length !== 1 ? 's' : ''}\r\n
\r\n
\r\n\r\n {/* Lista de Pacientes do Dia */}\r\n
\r\n {todayEvents.length === 0 ? (\r\n
\r\n \r\n

Nenhuma consulta agendada para este dia

\r\n

Agenda livre para este dia

\r\n
\r\n ) : (\r\n todayEvents.map((appointment) => {\r\n const paciente = pacientes.find(p => p.nome === appointment.title);\r\n return (\r\n \r\n
\r\n
\r\n
\r\n
\r\n
\r\n \r\n {appointment.title}\r\n
\r\n {paciente && (\r\n
\r\n CPF: {getPatientCpf(paciente)} • {getPatientAge(paciente)} anos\r\n
\r\n )}\r\n
\r\n
\r\n
\r\n \r\n {appointment.time}\r\n
\r\n
\r\n
\r\n {appointment.type}\r\n
\r\n
\r\n
\r\n
\r\n
\r\n Ver informações do paciente\r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n
\r\n );\r\n })\r\n )}\r\n
\r\n \r\n );\r\n };\r\n\r\n \r\n \r\n \r\n const renderLaudosSection = () => (\r\n
\r\n {\r\n setIsEditingLaudoForPatient(false);\r\n setPatientForLaudo(null);\r\n }}\r\n />\r\n
\r\n );\r\n\r\n // --- NOVO SISTEMA DE LAUDOS COMPLETO ---\r\n function LaudoManager({ isEditingForPatient, selectedPatientForLaudo, onClosePatientEditor }: { isEditingForPatient?: boolean; selectedPatientForLaudo?: any; onClosePatientEditor?: () => void }) {\r\n const [pacientesDisponiveis] = useState([\r\n { id: \"95170038\", nome: \"Ana Souza\", cpf: \"123.456.789-00\", idade: 42, sexo: \"Feminino\" },\r\n { id: \"93203056\", nome: \"Bruno Lima\", cpf: \"987.654.321-00\", idade: 33, sexo: \"Masculino\" },\r\n { id: \"92953542\", nome: \"Carla Menezes\", cpf: \"111.222.333-44\", idade: 67, sexo: \"Feminino\" },\r\n ]);\r\n\r\n const { reports, loadReports, loadReportById, loading: reportsLoading, createNewReport, updateExistingReport } = useReports();\r\n const [laudos, setLaudos] = useState([]);\r\n const [selectedRange, setSelectedRange] = useState<'todos'|'semana'|'mes'|'custom'>('mes');\r\n const [startDate, setStartDate] = useState(null);\r\n const [endDate, setEndDate] = useState(null);\r\n\r\n // helper to check if a date string is in range\r\n const isInRange = (dateStr: string | undefined, range: 'todos'|'semana'|'mes'|'custom') => {\r\n if (range === 'todos') return true;\r\n if (!dateStr) return false;\r\n const d = new Date(dateStr);\r\n if (isNaN(d.getTime())) return false;\r\n const now = new Date();\r\n \r\n if (range === 'semana') {\r\n const start = new Date(now);\r\n start.setDate(now.getDate() - now.getDay()); // sunday start\r\n const end = new Date(start);\r\n end.setDate(start.getDate() + 6);\r\n return d >= start && d <= end;\r\n }\r\n // mes\r\n return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth();\r\n };\r\n\r\n // helper: ensure report has paciente object populated (fetch by id if necessary)\r\n const ensurePaciente = async (report: any) => {\r\n if (!report) return report;\r\n try {\r\n if (!report.paciente) {\r\n const pid = report.patient_id ?? report.patient ?? report.paciente ?? null;\r\n if (pid) {\r\n try {\r\n const p = await buscarPacientePorId(String(pid));\r\n if (p) report.paciente = p;\r\n } catch (e) {\r\n // ignore\r\n }\r\n }\r\n }\r\n } catch (e) {\r\n // ignore\r\n }\r\n return report;\r\n };\r\n\r\n // When selectedRange changes (and isn't custom), compute start/end dates\r\n useEffect(() => {\r\n const now = new Date();\r\n if (selectedRange === 'todos') {\r\n setStartDate(null);\r\n setEndDate(null);\r\n return;\r\n }\r\n \r\n if (selectedRange === 'semana') {\r\n const start = new Date(now);\r\n start.setDate(now.getDate() - now.getDay()); // sunday\r\n const end = new Date(start);\r\n end.setDate(start.getDate() + 6);\r\n setStartDate(start.toISOString().slice(0,10));\r\n setEndDate(end.toISOString().slice(0,10));\r\n return;\r\n }\r\n if (selectedRange === 'mes') {\r\n const start = new Date(now.getFullYear(), now.getMonth(), 1);\r\n const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);\r\n setStartDate(start.toISOString().slice(0,10));\r\n setEndDate(end.toISOString().slice(0,10));\r\n return;\r\n }\r\n // custom: leave startDate/endDate as-is\r\n }, [selectedRange]);\r\n\r\n const filteredLaudos = (laudos || []).filter(l => {\r\n // If a specific start/end date is set, use that range\r\n if (startDate && endDate) {\r\n const ds = getReportDate(l);\r\n if (!ds) return false;\r\n const d = new Date(ds);\r\n if (isNaN(d.getTime())) return false;\r\n const start = new Date(startDate + 'T00:00:00');\r\n const end = new Date(endDate + 'T23:59:59');\r\n return d >= start && d <= end;\r\n }\r\n // Fallback to selectedRange heuristics\r\n if (!selectedRange) return true;\r\n const ds = getReportDate(l);\r\n return isInRange(ds, selectedRange);\r\n });\r\n\r\n function DateRangeButtons() {\r\n return (\r\n <>\r\n setSelectedRange('todos')}\r\n className=\"hover:bg-blue-50\"\r\n >\r\n Todos\r\n \r\n setSelectedRange('semana')}\r\n className=\"hover:bg-blue-50\"\r\n >\r\n Semana\r\n \r\n setSelectedRange('mes')}\r\n className=\"hover:bg-blue-50\"\r\n >\r\n Mês\r\n \r\n \r\n );\r\n }\r\n\r\n // SearchBox inserido aqui para acessar reports, setLaudos e loadReports\r\n function SearchBox() {\r\n const [searchTerm, setSearchTerm] = useState('');\r\n const [searching, setSearching] = useState(false);\r\n const { token } = useAuth();\r\n\r\n const isMaybeId = (s: string) => {\r\n const t = s.trim();\r\n if (!t) return false;\r\n if (t.includes('-') && t.length > 10) return true;\r\n if (t.toUpperCase().startsWith('REL-')) return true;\r\n const digits = t.replace(/\\D/g, '');\r\n if (digits.length >= 8) return true;\r\n return false;\r\n };\r\n\r\n const doSearch = async () => {\r\n const term = searchTerm.trim();\r\n if (!term) return;\r\n setSearching(true);\r\n try {\r\n if (isMaybeId(term)) {\r\n try {\r\n const r = await buscarRelatorioPorId(term);\r\n if (r) {\r\n // If token exists, attempt batch enrichment like useReports\r\n const enriched: any = { ...r };\r\n\r\n // Collect possible patient/doctor ids from payload\r\n const pidCandidates: string[] = [];\r\n const didCandidates: string[] = [];\r\n const pid = (r as any).patient_id ?? (r as any).patient ?? (r as any).paciente ?? null;\r\n if (pid) pidCandidates.push(String(pid));\r\n const possiblePatientName = (r as any).patient_name ?? (r as any).patient_full_name ?? (r as any).paciente?.full_name ?? (r as any).paciente?.nome ?? null;\r\n if (possiblePatientName) {\r\n enriched.paciente = enriched.paciente ?? {};\r\n enriched.paciente.full_name = possiblePatientName;\r\n }\r\n\r\n const did = (r as any).requested_by ?? (r as any).created_by ?? (r as any).executante ?? null;\r\n if (did) didCandidates.push(String(did));\r\n\r\n // If token available, perform batch fetch to get full patient/doctor objects\r\n if (token) {\r\n try {\r\n if (pidCandidates.length) {\r\n const patients = await buscarPacientesPorIds(pidCandidates);\r\n if (patients && patients.length) {\r\n const p = patients[0];\r\n enriched.paciente = enriched.paciente ?? {};\r\n enriched.paciente.full_name = enriched.paciente.full_name || p.full_name || (p as any).nome;\r\n enriched.paciente.id = enriched.paciente.id || p.id;\r\n enriched.paciente.cpf = enriched.paciente.cpf || p.cpf;\r\n }\r\n }\r\n if (didCandidates.length) {\r\n const doctors = await buscarMedicosPorIds(didCandidates);\r\n if (doctors && doctors.length) {\r\n const d = doctors[0];\r\n enriched.executante = enriched.executante || d.full_name || (d as any).nome;\r\n }\r\n }\r\n } catch (e) {\r\n // fallback: continue with payload-only enrichment\r\n console.warn('[SearchBox] batch enrichment failed, falling back to payload-only enrichment', e);\r\n }\r\n }\r\n\r\n // Final payload-only fallbacks (ensure id/cpf/order_number are populated)\r\n const possiblePatientId = (r as any).paciente?.id ?? (r as any).patient?.id ?? (r as any).patient_id ?? (r as any).patientId ?? (r as any).id ?? undefined;\r\n if (possiblePatientId && !enriched.paciente?.id) {\r\n enriched.paciente = enriched.paciente ?? {};\r\n enriched.paciente.id = possiblePatientId;\r\n }\r\n const possibleCpf = (r as any).patient_cpf ?? (r as any).paciente?.cpf ?? (r as any).patient?.cpf ?? null;\r\n if (possibleCpf) {\r\n enriched.paciente = enriched.paciente ?? {};\r\n enriched.paciente.cpf = possibleCpf;\r\n }\r\n const execName = (r as any).requested_by_name ?? (r as any).requester_name ?? (r as any).requestedByName ?? (r as any).executante_name ?? (r as any).created_by_name ?? (r as any).createdByName ?? (r as any).executante ?? (r as any).requested_by ?? (r as any).created_by ?? '';\r\n if (execName) enriched.executante = enriched.executante || execName;\r\n if ((r as any).order_number) enriched.order_number = (r as any).order_number;\r\n\r\n setLaudos([enriched]);\r\n return;\r\n }\r\n } catch (err: any) {\r\n console.warn('Relatório não encontrado por ID:', err);\r\n }\r\n }\r\n\r\n const lower = term.toLowerCase();\r\n const filtered = (reports || []).filter((x: any) => {\r\n const name = (x.paciente?.full_name || x.patient_name || x.patient_full_name || x.order_number || x.exame || x.exam || '').toString().toLowerCase();\r\n return name.includes(lower);\r\n });\r\n if (filtered.length) setLaudos(filtered);\r\n else setLaudos([]);\r\n } finally {\r\n setSearching(false);\r\n }\r\n };\r\n\r\n const handleKey = (e: React.KeyboardEvent) => {\r\n if (e.key === 'Enter') doSearch();\r\n };\r\n\r\n const handleClear = async () => {\r\n setSearchTerm('');\r\n await loadReports();\r\n setLaudos(reports || []);\r\n };\r\n\r\n return (\r\n
\r\n
\r\n setSearchTerm(e.target.value)}\r\n onKeyDown={handleKey}\r\n />\r\n \r\n \r\n \r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n );\r\n }\r\n\r\n // carregar laudos ao montar - somente dos pacientes atribuídos ao médico logado\r\n useEffect(() => {\r\n let mounted = true;\r\n (async () => {\r\n try {\r\n // obter assignments para o usuário logado\r\n const assignments = await import('@/lib/assignment').then(m => m.listAssignmentsForUser(user?.id || ''));\r\n const patientIds = Array.isArray(assignments) ? assignments.map(a => String(a.patient_id)).filter(Boolean) : [];\r\n\r\n if (patientIds.length === 0) {\r\n if (mounted) setLaudos([]);\r\n return;\r\n }\r\n\r\n // Tentar carregar todos os relatórios em uma única chamada usando in.(...)\r\n try {\r\n const reportsMod = await import('@/lib/reports');\r\n if (typeof reportsMod.listarRelatoriosPorPacientes === 'function') {\r\n const batch = await reportsMod.listarRelatoriosPorPacientes(patientIds);\r\n // Filtrar apenas relatórios criados/solicitados por este usuário (evita mostrar laudos de outros médicos)\r\n const mineOnly = (batch || []).filter((r: any) => {\r\n const requester = ((r.requested_by ?? r.created_by ?? r.executante ?? r.requestedBy ?? r.createdBy) || '').toString();\r\n return user?.id && requester && requester === user.id;\r\n });\r\n // Enrich reports with paciente objects so UI shows name/cpf immediately\r\n const enriched = await (async (reportsArr: any[]) => {\r\n if (!reportsArr || !reportsArr.length) return reportsArr;\r\n const pids = reportsArr.map(r => String(getReportPatientId(r))).filter(Boolean);\r\n if (!pids.length) return reportsArr;\r\n try {\r\n const patients = await buscarPacientesPorIds(pids);\r\n const map = new Map((patients || []).map((p: any) => [String(p.id), p]));\r\n return reportsArr.map(r => {\r\n const pid = String(getReportPatientId(r));\r\n return { ...r, paciente: r.paciente ?? map.get(pid) ?? r.paciente };\r\n });\r\n } catch (e) {\r\n return reportsArr;\r\n }\r\n })(mineOnly);\r\n if (mounted) setLaudos(enriched || []);\r\n } else {\r\n // fallback: 请求 por paciente individual\r\n const allReports: any[] = [];\r\n for (const pid of patientIds) {\r\n try {\r\n const rels = await import('@/lib/reports').then(m => m.listarRelatoriosPorPaciente(pid));\r\n if (Array.isArray(rels) && rels.length) {\r\n // filtrar por autor (requested_by / created_by / executante)\r\n const mine = rels.filter((r: any) => {\r\n const requester = ((r.requested_by ?? r.created_by ?? r.executante ?? r.requestedBy ?? r.createdBy) || '').toString();\r\n return user?.id && requester && requester === user.id;\r\n });\r\n if (mine.length) allReports.push(...mine);\r\n }\r\n } catch (err) {\r\n console.warn('[LaudoManager] falha ao carregar relatórios para paciente', pid, err);\r\n }\r\n }\r\n // enrich fallback results too\r\n const enrichedAll = await (async (reportsArr: any[]) => {\r\n if (!reportsArr || !reportsArr.length) return reportsArr;\r\n const pids = reportsArr.map(r => String(getReportPatientId(r))).filter(Boolean);\r\n if (!pids.length) return reportsArr;\r\n try {\r\n const patients = await buscarPacientesPorIds(pids);\r\n const map = new Map((patients || []).map((p: any) => [String(p.id), p]));\r\n return reportsArr.map(r => ({ ...r, paciente: r.paciente ?? map.get(String(getReportPatientId(r))) ?? r.paciente }));\r\n } catch (e) {\r\n return reportsArr;\r\n }\r\n })(allReports);\r\n if (mounted) setLaudos(enrichedAll);\r\n }\r\n } catch (err) {\r\n console.warn('[LaudoManager] erro ao carregar relatórios em batch, tentando por paciente individual', err);\r\n const allReports: any[] = [];\r\n for (const pid of patientIds) {\r\n try {\r\n const rels = await import('@/lib/reports').then(m => m.listarRelatoriosPorPaciente(pid));\r\n if (Array.isArray(rels) && rels.length) {\r\n const mine = rels.filter((r: any) => {\r\n const requester = ((r.requested_by ?? r.created_by ?? r.executante ?? r.requestedBy ?? r.createdBy) || '').toString();\r\n return user?.id && requester && requester === user.id;\r\n });\r\n if (mine.length) allReports.push(...mine);\r\n }\r\n } catch (e) {\r\n console.warn('[LaudoManager] falha ao carregar relatórios para paciente', pid, e);\r\n }\r\n }\r\n const enrichedAll = await (async (reportsArr: any[]) => {\r\n if (!reportsArr || !reportsArr.length) return reportsArr;\r\n const pids = reportsArr.map(r => String(getReportPatientId(r))).filter(Boolean);\r\n if (!pids.length) return reportsArr;\r\n try {\r\n const patients = await buscarPacientesPorIds(pids);\r\n const map = new Map((patients || []).map((p: any) => [String(p.id), p]));\r\n return reportsArr.map(r => ({ ...r, paciente: r.paciente ?? map.get(String(getReportPatientId(r))) ?? r.paciente }));\r\n } catch (e) {\r\n return reportsArr;\r\n }\r\n })(allReports);\r\n if (mounted) setLaudos(enrichedAll);\r\n }\r\n } catch (e) {\r\n console.warn('[LaudoManager] erro ao carregar laudos para pacientes atribuídos:', e);\r\n if (mounted) setLaudos(reports || []);\r\n }\r\n })();\r\n return () => { mounted = false; };\r\n }, [user?.id]);\r\n\r\n // sincroniza quando reports mudarem no hook (fallback)\r\n useEffect(() => {\r\n if (!laudos || laudos.length === 0) setLaudos(reports || []);\r\n }, [reports]);\r\n\r\n // Sort reports newest-first (more recent dates at the top)\r\n const sortedLaudos = React.useMemo(() => {\r\n const arr = (filteredLaudos || []).slice();\r\n arr.sort((a: any, b: any) => {\r\n try {\r\n const da = new Date(getReportDate(a) || 0).getTime() || 0;\r\n const db = new Date(getReportDate(b) || 0).getTime() || 0;\r\n return db - da;\r\n } catch (e) {\r\n return 0;\r\n }\r\n });\r\n return arr;\r\n }, [filteredLaudos]);\r\n\r\n const [activeTab, setActiveTab] = useState(\"descobrir\");\r\n const [laudoSelecionado, setLaudoSelecionado] = useState(null);\r\n const [isViewing, setIsViewing] = useState(false);\r\n const [isCreatingNew, setIsCreatingNew] = useState(false);\r\n\r\n\r\n\r\n\r\n return (\r\n
\r\n {/* Header */}\r\n
\r\n
\r\n
\r\n

Gerenciamento de Laudo

\r\n

Nesta seção você pode gerenciar todos os laudos gerados.

\r\n
\r\n \r\n
\r\n
\r\n\r\n {/* Tabs */}\r\n
\r\n
\r\n setActiveTab(\"descobrir\")}\r\n className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${\r\n activeTab === \"descobrir\"\r\n ? \"border-blue-500 text-blue-600\"\r\n : \"border-transparent text-muted-foreground hover:text-foreground\"\r\n }`}\r\n >\r\n A descobrir\r\n \r\n
\r\n\r\n {/* Filtros */}\r\n
\r\n
\r\n
\r\n {/* Search input integrado com busca por ID */}\r\n \r\n
\r\n \r\n
\r\n
\r\n \r\n { setStartDate(e.target.value); setSelectedRange('custom'); }} className=\"p-1 text-sm h-10\" />\r\n -\r\n { setEndDate(e.target.value); setSelectedRange('custom'); }} className=\"p-1 text-sm h-10\" />\r\n
\r\n
\r\n\r\n
\r\n {/* date range buttons: Semana / Mês */}\r\n \r\n
\r\n\r\n {/* Filtros e pesquisa removidos por solicitação */}\r\n
\r\n
\r\n\r\n {/* Tabela para desktop e cards empilháveis para mobile */}\r\n
\r\n {/* Desktop / tablet (md+) - tabela com scroll horizontal */}\r\n
\r\n \r\n \r\n \r\n Pedido\r\n Data\r\n Prazo\r\n Paciente\r\n Executante/Solicitante\r\n Exame/Classificação\r\n Ação\r\n \r\n \r\n \r\n {sortedLaudos.map((laudo, idx) => (\r\n \r\n \r\n
\r\n {laudo.urgente && (\r\n
\r\n )}\r\n \r\n {getReportPatientName(laudo) || laudo.order_number || getShortId(laudo.id)}\r\n \r\n
\r\n
\r\n \r\n
\r\n
{formatReportDate(getReportDate(laudo))}
\r\n
{laudo?.hora || new Date(laudo?.data || laudo?.created_at || laudo?.due_at || Date.now()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
\r\n
\r\n
\r\n \r\n
\r\n
{(laudo?.prazo ?? laudo?.due_at) ? formatReportDate(laudo?.due_at ?? laudo?.prazo) : '-'}
\r\n
{\r\n (() => {\r\n // prefer explicit fields\r\n const explicit = laudo?.prazo_hora ?? laudo?.due_time ?? laudo?.hora ?? null;\r\n if (explicit) return explicit;\r\n // fallback: try to parse due_at / prazo datetime and extract time\r\n const due = laudo?.due_at ?? laudo?.prazo ?? laudo?.dueDate ?? laudo?.data ?? null;\r\n if (!due) return '-';\r\n try {\r\n const d = new Date(due);\r\n if (isNaN(d.getTime())) return '-';\r\n return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });\r\n } catch (e) {\r\n return '-';\r\n }\r\n })()\r\n }
\r\n
\r\n
\r\n \r\n
\r\n
\r\n \r\n
\r\n
{getReportPatientName(laudo) || '—'}
\r\n
{getReportPatientCpf(laudo) ? `CPF: ${getReportPatientCpf(laudo)}` : ''}
\r\n
\r\n
\r\n {\r\n (() => {\r\n const possibleName = laudo.requested_by_name ?? laudo.requester_name ?? laudo.requestedByName ?? laudo.executante_name ?? laudo.executante ?? laudo.executante_name ?? laudo.executante;\r\n if (possibleName && typeof possibleName === 'string' && possibleName.trim().length) return possibleName;\r\n const possibleId = (laudo.requested_by ?? laudo.created_by ?? laudo.executante ?? laudo.requestedBy ?? laudo.createdBy) || '';\r\n if (possibleId && user?.id && possibleId === user.id) return (profileData as any)?.nome || user?.name || possibleId;\r\n return possibleName || possibleId || '-';\r\n })()\r\n }\r\n {getReportExam(laudo) || \"-\"}\r\n \r\n
\r\n {\r\n try {\r\n const full = (laudo?.id || laudo?.order_number) ? await loadReportById(String(laudo?.id ?? laudo?.order_number)) : laudo;\r\n await ensurePaciente(full);\r\n setLaudoSelecionado(full);\r\n setIsViewing(true);\r\n } catch (e) {\r\n // fallback\r\n setLaudoSelecionado(laudo);\r\n setIsViewing(true);\r\n }\r\n }}\r\n className=\"flex items-center gap-1 hover:bg-blue-50 dark:hover:bg-accent dark:hover:text-accent-foreground\"\r\n >\r\n \r\n Ver Laudo\r\n \r\n {\r\n setPatientForLaudo(laudo);\r\n setIsEditingLaudoForPatient(true);\r\n }}\r\n className=\"flex items-center gap-1 bg-green-600 hover:bg-green-700 text-white\"\r\n title=\"Editar laudo para este paciente\"\r\n >\r\n \r\n Editar Laudo\r\n \r\n
\r\n
\r\n
\r\n ))}\r\n
\r\n
\r\n
\r\n\r\n {/* Mobile - cards empilháveis */}\r\n
\r\n {sortedLaudos.map((laudo, idx) => (\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
{getReportExam(laudo) || '-'}
\r\n
{formatReportDate(getReportDate(laudo))} {laudo?.hora ? `• ${laudo.hora}` : ''}
\r\n
\r\n
{getReportPatientName(laudo) ? getShortId(laudo.id) : ''}
\r\n
\r\n
\r\n
{getReportPatientName(laudo) || '—'}
\r\n
{getReportPatientCpf(laudo) ? `CPF: ${getReportPatientCpf(laudo)}` : ''}
\r\n
\r\n
\r\n
\r\n
{\r\n (() => {\r\n const possibleName = laudo.requested_by_name ?? laudo.requester_name ?? laudo.requestedByName ?? laudo.executante_name ?? laudo.executante ?? laudo.executante_name ?? laudo.executante;\r\n if (possibleName && typeof possibleName === 'string' && possibleName.trim().length) return possibleName;\r\n const possibleId = (laudo.requested_by ?? laudo.created_by ?? laudo.executante ?? laudo.requestedBy ?? laudo.createdBy) || '';\r\n if (possibleId && user?.id && possibleId === user.id) return (profileData as any)?.nome || user?.name || possibleId;\r\n return possibleName || possibleId || '-';\r\n })()\r\n }
\r\n
\r\n {\r\n setLaudoSelecionado(laudo);\r\n setIsViewing(true);\r\n }}\r\n className=\"flex items-center gap-1\"\r\n >\r\n \r\n \r\n {\r\n setPatientForLaudo(laudo);\r\n setIsEditingLaudoForPatient(true);\r\n }}\r\n className=\"flex items-center gap-1 bg-green-600 hover:bg-green-700 text-white\"\r\n title=\"Editar laudo\"\r\n >\r\n \r\n \r\n
\r\n
\r\n
\r\n
\r\n ))}\r\n
\r\n
\r\n
\r\n\r\n {/* Visualizador de Laudo */}\r\n {isViewing && laudoSelecionado && (\r\n setIsViewing(false)} />\r\n )}\r\n\r\n {/* Editor para Novo Laudo */}\r\n {isCreatingNew && (\r\n setIsCreatingNew(false)}\r\n isNewLaudo={true}\r\n createNewReport={createNewReport}\r\n updateExistingReport={updateExistingReport}\r\n reloadReports={loadReports}\r\n onSaved={async (r:any) => {\r\n try {\r\n // If report has an id, fetch full report and open viewer\r\n if (r && (r.id || r.order_number)) {\r\n const id = r.id ?? r.order_number;\r\n const full = await loadReportById(String(id));\r\n await ensurePaciente(full);\r\n // prepend to laudos list so it appears immediately\r\n setLaudos(prev => [full, ...(prev || [])]);\r\n setLaudoSelecionado(full);\r\n setIsViewing(true);\r\n } else {\r\n setLaudoSelecionado(r);\r\n setIsViewing(true);\r\n }\r\n // refresh global reports list too\r\n try { await loadReports(); } catch {}\r\n } catch (e) {\r\n // fallback: open what we have\r\n setLaudoSelecionado(r);\r\n setIsViewing(true);\r\n }\r\n }}\r\n />\r\n )}\r\n\r\n {/* Editor para Paciente Específico */}\r\n {isEditingForPatient && selectedPatientForLaudo && (\r\n {})}\r\n isNewLaudo={!selectedPatientForLaudo?.id}\r\n preSelectedPatient={selectedPatientForLaudo.paciente || selectedPatientForLaudo}\r\n createNewReport={createNewReport}\r\n updateExistingReport={updateExistingReport}\r\n reloadReports={loadReports}\r\n onSaved={async (r:any) => {\r\n try {\r\n if (r && (r.id || r.order_number)) {\r\n const id = r.id ?? r.order_number;\r\n const full = await loadReportById(String(id));\r\n await ensurePaciente(full);\r\n setLaudos(prev => [full, ...(prev || [])]);\r\n setLaudoSelecionado(full);\r\n setIsViewing(true);\r\n } else {\r\n setLaudoSelecionado(r);\r\n setIsViewing(true);\r\n }\r\n try { await loadReports(); } catch {}\r\n } catch (e) {\r\n setLaudoSelecionado(r);\r\n setIsViewing(true);\r\n }\r\n }}\r\n />\r\n )}\r\n
\r\n );\r\n }\r\n\r\n // Visualizador de Laudo (somente leitura)\r\n function LaudoViewer({ laudo, onClose }: { laudo: any; onClose: () => void }) {\r\n return (\r\n
\r\n
\r\n {/* Header */}\r\n
\r\n
\r\n

Visualizar Laudo

\r\n

\r\n Paciente: {getPatientName(laudo?.paciente) || getPatientName(laudo) || '—'} | CPF: {getReportPatientCpf(laudo) ?? laudo?.patient_cpf ?? '-'} | {laudo?.especialidade ?? laudo?.exame ?? '-'}\r\n

\r\n
\r\n \r\n
\r\n\r\n {/* Content */}\r\n
\r\n
\r\n {/* Header do Laudo */}\r\n
\r\n

LAUDO MÉDICO - {(laudo.especialidade ?? laudo.exame ?? '').toString().toUpperCase()}

\r\n

\r\n Data: {formatReportDate(getReportDate(laudo))}\r\n

\r\n
\r\n\r\n {/* Dados do Paciente */}\r\n
\r\n

Dados do Paciente:

\r\n
\r\n

Nome: {getPatientName(laudo?.paciente) || getPatientName(laudo) || '-'}

\r\n

CPF: {getPatientCpf(laudo?.paciente) ?? laudo?.patient_cpf ?? '-'}

\r\n

Idade: {getPatientAge(laudo?.paciente) ? `${getPatientAge(laudo?.paciente)} anos` : (getPatientAge(laudo) ? `${getPatientAge(laudo)} anos` : '-')}

\r\n

Sexo: {getPatientSex(laudo?.paciente) ?? getPatientSex(laudo) ?? '-'}

\r\n

CID: {laudo?.cid ?? laudo?.cid_code ?? '-'}

\r\n
\r\n
\r\n\r\n {/* Conteúdo do Laudo */}\r\n
\r\n
') \r\n }}\r\n />\r\n
\r\n\r\n {/* Exame */}\r\n {((laudo.exame ?? laudo.exam ?? laudo.especialidade ?? laudo.report_type) || '').toString().length > 0 && (\r\n
\r\n

Exame / Especialidade:

\r\n

{laudo.exame ?? laudo.exam ?? laudo.especialidade ?? laudo.report_type}

\r\n
\r\n )}\r\n\r\n {/* Diagnóstico */}\r\n {((laudo.diagnostico ?? laudo.diagnosis) || '').toString().length > 0 && (\r\n
\r\n

Diagnóstico:

\r\n

{laudo.diagnostico ?? laudo.diagnosis}

\r\n
\r\n )}\r\n\r\n {/* Conclusão */}\r\n {((laudo.conclusao ?? laudo.conclusion) || '').toString().length > 0 && (\r\n
\r\n

Conclusão:

\r\n

{laudo.conclusao ?? laudo.conclusion}

\r\n
\r\n )}\r\n\r\n {/* Diagnóstico e Conclusão */}\r\n {laudo.diagnostico && (\r\n
\r\n

Diagnóstico:

\r\n

{laudo.diagnostico}

\r\n
\r\n )}\r\n\r\n {laudo.conclusao && (\r\n
\r\n

Conclusão:

\r\n

{laudo.conclusao}

\r\n
\r\n )}\r\n\r\n {/* Assinatura */}\r\n
\r\n
\r\n {(() => {\r\n const signatureName = laudo?.created_by_name ?? laudo?.createdByName ?? ((laudo?.created_by && user?.id && laudo.created_by === user.id) ? profileData.nome : (laudo?.created_by_name || profileData.nome));\r\n return (\r\n <>\r\n

{signatureName}

\r\n

{profileData.crm ? `CRM: ${String(profileData.crm).replace(/^(?:CRM\\s*)+/i, '').trim()}` : 'CRM não informado'}{laudo.especialidade ? ` - ${laudo.especialidade}` : ''}

\r\n

Data: {formatReportDate(getReportDate(laudo))}

\r\n \r\n );\r\n })()}\r\n
\r\n
\r\n
\r\n\r\n {/* Footer */}\r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n
\r\n );\r\n }\r\n\r\n // Editor de Laudo Avançado (para novos laudos)\r\n function LaudoEditor({ pacientes, laudo, onClose, isNewLaudo, preSelectedPatient, createNewReport, updateExistingReport, reloadReports, onSaved }: { pacientes?: any[]; laudo?: any; onClose: () => void; isNewLaudo?: boolean; preSelectedPatient?: any; createNewReport?: (data: any) => Promise; updateExistingReport?: (id: string, data: any) => Promise; reloadReports?: () => Promise; onSaved?: (r:any) => void }) {\r\n // Import useToast at the top level of the component\r\n const { toast } = require('@/hooks/use-toast').useToast();\r\n const [activeTab, setActiveTab] = useState(\"editor\");\r\n const [content, setContent] = useState(laudo?.conteudo || \"\");\r\n const [showPreview, setShowPreview] = useState(false);\r\n const [pacienteSelecionado, setPacienteSelecionado] = useState(preSelectedPatient || null);\r\n const [listaPacientes, setListaPacientes] = useState([]);\r\n // Novo: campos para solicitante e prazo\r\n // solicitanteId será enviado ao backend (sempre o id do usuário logado)\r\n const [solicitanteId, setSolicitanteId] = useState(user?.id || \"\");\r\n // displaySolicitante é apenas para exibição (nome do usuário) e NÃO é enviado ao backend\r\n // Prefer profileData.nome (nome do médico carregado) — cai back para user.name ou email\r\n const displaySolicitante = ((profileData as any) && ((profileData as any).nome || (profileData as any).nome_social)) || user?.name || (user?.profile as any)?.full_name || user?.email || '';\r\n const [prazoDate, setPrazoDate] = useState(\"\");\r\n const [prazoTime, setPrazoTime] = useState(\"\");\r\n\r\n // Pega token do usuário logado (passado explicitamente para listarPacientes)\r\n const { token } = useAuth();\r\n\r\n // Carregar pacientes reais do Supabase ao abrir o modal ou quando o token mudar\r\n useEffect(() => {\r\n async function fetchPacientes() {\r\n try {\r\n if (!token) {\r\n setListaPacientes([]);\r\n return;\r\n }\r\n const pacientes = await listarPacientes();\r\n setListaPacientes(pacientes || []);\r\n } catch (err) {\r\n console.warn('Erro ao carregar pacientes:', err);\r\n setListaPacientes([]);\r\n }\r\n }\r\n fetchPacientes();\r\n }, [token]);\r\n const [campos, setCampos] = useState({\r\n cid: laudo?.cid || \"\",\r\n diagnostico: laudo?.diagnostico || \"\",\r\n conclusao: laudo?.conclusao || \"\",\r\n exame: laudo?.exame || \"\",\r\n especialidade: laudo?.especialidade || \"\",\r\n mostrarData: true,\r\n mostrarAssinatura: true\r\n });\r\n const [imagens, setImagens] = useState([]);\r\n const [templates] = useState([\r\n \"Exame normal, sem alterações significativas\",\r\n \"Paciente em acompanhamento ambulatorial\",\r\n \"Recomenda-se retorno em 30 dias\",\r\n \"Alterações compatíveis com processo inflamatório\",\r\n \"Resultado dentro dos parâmetros de normalidade\",\r\n \"Recomendo seguimento com especialista\"\r\n ]);\r\n\r\n\r\n const sigCanvasRef = useRef(null);\r\n\r\n // Estado para imagem da assinatura\r\n const [assinaturaImg, setAssinaturaImg] = useState(null);\r\n\r\n useEffect(() => {\r\n if (!sigCanvasRef.current) return;\r\n const handleEnd = () => {\r\n const url = sigCanvasRef.current.getTrimmedCanvas().toDataURL('image/png');\r\n setAssinaturaImg(url);\r\n };\r\n const canvas = sigCanvasRef.current;\r\n if (canvas && canvas.canvas) {\r\n canvas.canvas.addEventListener('mouseup', handleEnd);\r\n canvas.canvas.addEventListener('touchend', handleEnd);\r\n }\r\n return () => {\r\n if (canvas && canvas.canvas) {\r\n canvas.canvas.removeEventListener('mouseup', handleEnd);\r\n canvas.canvas.removeEventListener('touchend', handleEnd);\r\n }\r\n };\r\n }, [sigCanvasRef]);\r\n\r\n const handleClearSignature = () => {\r\n if (sigCanvasRef.current) {\r\n sigCanvasRef.current.clear();\r\n }\r\n setAssinaturaImg(null);\r\n };\r\n\r\n // Carregar dados do laudo existente quando disponível (mais robusto: suporta vários nomes de campo)\r\n useEffect(() => {\r\n if (laudo && !isNewLaudo) {\r\n // Conteúdo: aceita 'conteudo', 'content_html', 'contentHtml', 'content'\r\n const contentValue = laudo.conteudo ?? laudo.content_html ?? laudo.contentHtml ?? laudo.content ?? \"\";\r\n setContent(contentValue);\r\n\r\n // Campos: use vários fallbacks\r\n const cidValue = laudo.cid ?? laudo.cid_code ?? '';\r\n const diagnosticoValue = laudo.diagnostico ?? laudo.diagnosis ?? '';\r\n const conclusaoValue = laudo.conclusao ?? laudo.conclusion ?? '';\r\n const exameValue = laudo.exame ?? laudo.exam ?? laudo.especialidade ?? '';\r\n const especialidadeValue = laudo.especialidade ?? laudo.exame ?? laudo.exam ?? '';\r\n const mostrarDataValue = typeof laudo.hide_date === 'boolean' ? !laudo.hide_date : true;\r\n const mostrarAssinaturaValue = typeof laudo.hide_signature === 'boolean' ? !laudo.hide_signature : true;\r\n\r\n setCampos({\r\n cid: cidValue,\r\n diagnostico: diagnosticoValue,\r\n conclusao: conclusaoValue,\r\n exame: exameValue,\r\n especialidade: especialidadeValue,\r\n mostrarData: mostrarDataValue,\r\n mostrarAssinatura: mostrarAssinaturaValue\r\n });\r\n\r\n // Paciente: não sobrescrever se já existe preSelectedPatient ou pacienteSelecionado\r\n if (!pacienteSelecionado) {\r\n const pacienteFromLaudo = laudo.paciente ?? laudo.patient ?? null;\r\n if (pacienteFromLaudo) {\r\n setPacienteSelecionado(pacienteFromLaudo);\r\n } else if (laudo.patient_id && listaPacientes && listaPacientes.length) {\r\n const found = listaPacientes.find(p => String(p.id) === String(laudo.patient_id));\r\n if (found) setPacienteSelecionado(found);\r\n }\r\n }\r\n\r\n // preencher solicitanteId/prazo quando existe laudo (edição)\r\n // preferimos manter o solicitanteId como o user id; se o laudo tiver requested_by que pareça um id, usamos ele\r\n const possibleRequestedById = laudo.requested_by ?? laudo.created_by ?? null;\r\n if (possibleRequestedById && typeof possibleRequestedById === 'string' && possibleRequestedById.length > 5) {\r\n setSolicitanteId(possibleRequestedById);\r\n } else {\r\n setSolicitanteId(user?.id || \"\");\r\n }\r\n\r\n const dueRaw = laudo.due_at ?? laudo.prazo ?? laudo.dueDate ?? laudo.data ?? null;\r\n if (dueRaw) {\r\n try {\r\n const d = new Date(dueRaw);\r\n if (!isNaN(d.getTime())) {\r\n setPrazoDate(d.toISOString().slice(0,10));\r\n setPrazoTime(d.toTimeString().slice(0,5));\r\n }\r\n } catch (e) {\r\n // ignore invalid date\r\n }\r\n }\r\n\r\n // assinatura: aceitar vários campos possíveis\r\n const sig = laudo.assinaturaImg ?? laudo.signature_image ?? laudo.signature ?? laudo.sign_image ?? null;\r\n if (sig) setAssinaturaImg(sig);\r\n }\r\n }, [laudo, isNewLaudo, pacienteSelecionado, listaPacientes, user]);\r\n\r\n // Histórico para desfazer/refazer\r\n const [history, setHistory] = useState([]);\r\n const [historyIndex, setHistoryIndex] = useState(-1);\r\n\r\n // Atualiza histórico ao digitar\r\n useEffect(() => {\r\n if (history[historyIndex] !== content) {\r\n const newHistory = history.slice(0, historyIndex + 1);\r\n setHistory([...newHistory, content]);\r\n setHistoryIndex(newHistory.length);\r\n }\r\n // eslint-disable-next-line\r\n }, [content]);\r\n\r\n const handleUndo = () => {\r\n if (historyIndex > 0) {\r\n setContent(history[historyIndex - 1]);\r\n setHistoryIndex(historyIndex - 1);\r\n }\r\n };\r\n const handleRedo = () => {\r\n if (historyIndex < history.length - 1) {\r\n setContent(history[historyIndex + 1]);\r\n setHistoryIndex(historyIndex + 1);\r\n }\r\n };\r\n\r\n // Formatação avançada\r\n const formatText = (type: string, value?: any) => {\r\n const textarea = document.querySelector('textarea') as HTMLTextAreaElement;\r\n if (!textarea) return;\r\n const start = textarea.selectionStart;\r\n const end = textarea.selectionEnd;\r\n const selectedText = textarea.value.substring(start, end);\r\n let formattedText = \"\";\r\n switch(type) {\r\n case \"bold\":\r\n formattedText = selectedText ? `**${selectedText}**` : \"**texto em negrito**\";\r\n break;\r\n case \"italic\":\r\n formattedText = selectedText ? `*${selectedText}*` : \"*texto em itálico*\";\r\n break;\r\n case \"underline\":\r\n formattedText = selectedText ? `__${selectedText}__` : \"__texto sublinhado__\";\r\n break;\r\n case \"list-ul\":\r\n formattedText = selectedText ? selectedText.split('\\n').map(l => `• ${l}`).join('\\n') : \"• item da lista\";\r\n break;\r\n case \"list-ol\":\r\n formattedText = selectedText ? selectedText.split('\\n').map((l,i) => `${i+1}. ${l}`).join('\\n') : \"1. item da lista\";\r\n break;\r\n case \"indent\":\r\n formattedText = selectedText ? selectedText.split('\\n').map(l => ` ${l}`).join('\\n') : \" \";\r\n break;\r\n case \"outdent\":\r\n formattedText = selectedText ? selectedText.split('\\n').map(l => l.replace(/^\\s{1,4}/, \"\")).join('\\n') : \"\";\r\n break;\r\n case \"align-left\":\r\n formattedText = selectedText ? `[left]${selectedText}[/left]` : \"[left]Texto à esquerda[/left]\";\r\n break;\r\n case \"align-center\":\r\n formattedText = selectedText ? `[center]${selectedText}[/center]` : \"[center]Texto centralizado[/center]\";\r\n break;\r\n case \"align-right\":\r\n formattedText = selectedText ? `[right]${selectedText}[/right]` : \"[right]Texto à direita[/right]\";\r\n break;\r\n case \"align-justify\":\r\n formattedText = selectedText ? `[justify]${selectedText}[/justify]` : \"[justify]Texto justificado[/justify]\";\r\n break;\r\n case \"font-size\":\r\n formattedText = selectedText ? `[size=${value}]${selectedText}[/size]` : `[size=${value}]Texto tamanho ${value}[/size]`;\r\n break;\r\n case \"font-family\":\r\n formattedText = selectedText ? `[font=${value}]${selectedText}[/font]` : `[font=${value}]${value}[/font]`;\r\n break;\r\n case \"font-color\":\r\n formattedText = selectedText ? `[color=${value}]${selectedText}[/color]` : `[color=${value}]${value}[/color]`;\r\n break;\r\n default:\r\n return;\r\n }\r\n const newText = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);\r\n setContent(newText);\r\n };\r\n\r\n const insertTemplate = (template: string) => {\r\n setContent((prev: string) => prev ? `${prev}\\n\\n${template}` : template);\r\n };\r\n\r\n const handleImageUpload = (e: React.ChangeEvent) => {\r\n const files = Array.from(e.target.files || []);\r\n files.forEach(file => {\r\n const reader = new FileReader();\r\n reader.onload = (e) => {\r\n setImagens(prev => [...prev, {\r\n id: Date.now() + Math.random(),\r\n name: file.name,\r\n url: e.target?.result,\r\n type: file.type\r\n }]);\r\n };\r\n reader.readAsDataURL(file);\r\n });\r\n };\r\n\r\n const processContent = (content: string) => {\r\n return content\r\n .replace(/\\*\\*(.*?)\\*\\*/g, '$1')\r\n .replace(/\\*(.*?)\\*/g, '$1')\r\n .replace(/__(.*?)__/g, '$1')\r\n .replace(/\\[left\\]([\\s\\S]*?)\\[\\/left\\]/g, '
$1
')\r\n .replace(/\\[center\\]([\\s\\S]*?)\\[\\/center\\]/g, '
$1
')\r\n .replace(/\\[right\\]([\\s\\S]*?)\\[\\/right\\]/g, '
$1
')\r\n .replace(/\\[justify\\]([\\s\\S]*?)\\[\\/justify\\]/g, '
$1
')\r\n .replace(/\\[size=(\\d+)\\]([\\s\\S]*?)\\[\\/size\\]/g, '$2')\r\n .replace(/\\[font=([^\\]]+)\\]([\\s\\S]*?)\\[\\/font\\]/g, '$2')\r\n .replace(/\\[color=([^\\]]+)\\]([\\s\\S]*?)\\[\\/color\\]/g, '$2')\r\n .replace(/{{sexo_paciente}}/g, pacienteSelecionado?.sexo || laudo?.paciente?.sexo || '[SEXO]')\r\n .replace(/{{diagnostico}}/g, campos.diagnostico || '[DIAGNÓSTICO]')\r\n .replace(/{{conclusao}}/g, campos.conclusao || '[CONCLUSÃO]')\r\n .replace(/\\n/g, '
');\r\n };\r\n\r\n return (\r\n
\r\n
\r\n {/* Header */}\r\n
\r\n
\r\n
\r\n

\r\n {isNewLaudo ? \"Novo Laudo Médico\" : \"Editar Laudo Existente\"}\r\n

\r\n {isNewLaudo ? (\r\n

\r\n Crie um novo laudo selecionando um paciente\r\n

\r\n ) : (\r\n

\r\n Paciente: {getPatientName(pacienteSelecionado) || getPatientName(laudo?.paciente) || getPatientName(laudo) || '-'} | CPF: {getReportPatientCpf(laudo) ?? laudo?.patient_cpf ?? '-'} | {laudo?.especialidade}\r\n

\r\n )}\r\n
\r\n \r\n
\r\n\r\n {/* Seleção de Paciente (apenas para novos laudos) */}\r\n {isNewLaudo && (\r\n
\r\n {!pacienteSelecionado ? (\r\n
\r\n \r\n \r\n
\r\n ) : (\r\n
\r\n
\r\n
{getPatientName(pacienteSelecionado)}
\r\n
\r\n {getPatientCpf(pacienteSelecionado) ? `CPF: ${getPatientCpf(pacienteSelecionado)} | ` : ''}\r\n {pacienteSelecionado?.birth_date ? `Nascimento: ${pacienteSelecionado.birth_date}` : (getPatientAge(pacienteSelecionado) ? `Idade: ${getPatientAge(pacienteSelecionado)} anos` : '')}\r\n {getPatientSex(pacienteSelecionado) ? ` | Sexo: ${getPatientSex(pacienteSelecionado)}` : ''}\r\n
\r\n
\r\n {!preSelectedPatient && (\r\n setPacienteSelecionado(null)}\r\n >\r\n Trocar Paciente\r\n \r\n )}\r\n
\r\n )}\r\n {/* Novos campos: Solicitante e Prazo */}\r\n
\r\n
\r\n \r\n {/* Mostrar o nome do usuário logado de forma estática (não editável) */}\r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n setPrazoDate(e.target.value)} />\r\n setPrazoTime(e.target.value)} />\r\n
\r\n

Defina a data e hora do prazo (opcional).

\r\n
\r\n
\r\n
\r\n )}\r\n
\r\n\r\n {/* Tabs */}\r\n
\r\n {/* Informações tab removed - only Editor/Imagens/Campos/Pré-visualização remain */}\r\n setActiveTab(\"editor\")}\r\n className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${\r\n activeTab === \"editor\"\r\n ? \"border-blue-500 text-blue-600\"\r\n : \"border-transparent text-gray-600 dark:text-muted-foreground dark:hover:text-foreground dark:hover:bg-blue-900\"\r\n }`}\r\n style={{\r\n backgroundColor: activeTab === \"editor\" ? undefined : \"transparent\"\r\n }}\r\n onMouseEnter={(e) => {\r\n if (activeTab !== \"editor\") {\r\n e.currentTarget.style.backgroundColor = \"transparent\";\r\n e.currentTarget.style.color = \"#4B5563\";\r\n }\r\n }}\r\n onMouseLeave={(e) => {\r\n if (activeTab !== \"editor\") {\r\n e.currentTarget.style.backgroundColor = \"transparent\";\r\n e.currentTarget.style.color = \"#4B5563\";\r\n }\r\n }}\r\n >\r\n \r\n Editor\r\n \r\n setActiveTab(\"imagens\")}\r\n className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${\r\n activeTab === \"imagens\"\r\n ? \"border-blue-500 text-blue-600\"\r\n : \"border-transparent text-gray-600 dark:text-muted-foreground dark:hover:text-foreground dark:hover:bg-blue-900\"\r\n }`}\r\n style={{\r\n backgroundColor: activeTab === \"imagens\" ? undefined : \"transparent\"\r\n }}\r\n onMouseEnter={(e) => {\r\n if (activeTab !== \"imagens\") {\r\n e.currentTarget.style.backgroundColor = \"transparent\";\r\n e.currentTarget.style.color = \"#4B5563\";\r\n }\r\n }}\r\n onMouseLeave={(e) => {\r\n if (activeTab !== \"imagens\") {\r\n e.currentTarget.style.backgroundColor = \"transparent\";\r\n e.currentTarget.style.color = \"#4B5563\";\r\n }\r\n }}\r\n >\r\n \r\n Imagens ({imagens.length})\r\n \r\n setActiveTab(\"campos\")}\r\n className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${\r\n activeTab === \"campos\"\r\n ? \"border-blue-500 text-blue-600\"\r\n : \"border-transparent text-gray-600 dark:text-muted-foreground dark:hover:text-foreground dark:hover:bg-blue-900\"\r\n }`}\r\n style={{\r\n backgroundColor: activeTab === \"campos\" ? undefined : \"transparent\"\r\n }}\r\n onMouseEnter={(e) => {\r\n if (activeTab !== \"campos\") {\r\n e.currentTarget.style.backgroundColor = \"transparent\";\r\n e.currentTarget.style.color = \"#4B5563\";\r\n }\r\n }}\r\n onMouseLeave={(e) => {\r\n if (activeTab !== \"campos\") {\r\n e.currentTarget.style.backgroundColor = \"transparent\";\r\n e.currentTarget.style.color = \"#4B5563\";\r\n }\r\n }}\r\n >\r\n \r\n Campos\r\n \r\n setShowPreview(!showPreview)}\r\n className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${\r\n showPreview\r\n ? \"border-green-500 text-green-600\"\r\n : \"border-transparent text-gray-600 dark:text-muted-foreground dark:hover:text-foreground dark:hover:bg-blue-900\"\r\n }`}\r\n style={{\r\n backgroundColor: !showPreview ? \"transparent\" : undefined\r\n }}\r\n onMouseEnter={(e) => {\r\n if (!showPreview) {\r\n e.currentTarget.style.backgroundColor = \"transparent\";\r\n e.currentTarget.style.color = \"#4B5563\";\r\n }\r\n }}\r\n onMouseLeave={(e) => {\r\n if (!showPreview) {\r\n e.currentTarget.style.backgroundColor = \"transparent\";\r\n e.currentTarget.style.color = \"#4B5563\";\r\n }\r\n }}\r\n >\r\n \r\n {showPreview ? \"Ocultar\" : \"Pré-visualização\"}\r\n \r\n
\r\n\r\n {/* Content */}\r\n
\r\n {/* Left Panel */}\r\n
\r\n {/* 'Informações' section removed to keep editor-only experience */}\r\n\r\n {activeTab === \"editor\" && (\r\n
\r\n {/* Toolbar */}\r\n
\r\n
\r\n {/* Tamanho da fonte */}\r\n \r\n formatText('font-size', e.target.value)}\r\n className=\"w-14 border rounded px-1 py-0.5 text-xs mr-2\"\r\n title=\"Tamanho da fonte\"\r\n />\r\n {/* Família da fonte */}\r\n \r\n formatText('font-family', e.target.value)}\r\n className=\"border rounded px-1 py-0.5 text-xs mr-2 bg-white text-gray-900 dark:bg-gray-800 dark:text-white\"\r\n style={{ minWidth: 140, fontWeight: 500 }}\r\n title=\"Família da fonte\"\r\n >\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n {/* Cor da fonte */}\r\n \r\n formatText('font-color', e.target.value)}\r\n className=\"w-6 h-6 border rounded mr-2\"\r\n title=\"Cor da fonte\"\r\n />\r\n {/* Alinhamento */}\r\n \r\n \r\n \r\n \r\n {/* Listas */}\r\n \r\n \r\n {/* Recuo */}\r\n \r\n \r\n {/* Desfazer/Refazer */}\r\n \r\n
\r\n {templates.map((template, idx) => (\r\n insertTemplate(template)}\r\n >\r\n {template.substring(0, 30)}...\r\n \r\n ))}\r\n
\r\n
\r\n
\r\n\r\n {/* Editor */}\r\n
\r\n setContent(e.target.value)}\r\n placeholder=\"Digite o conteúdo do laudo aqui. Use ** para negrito, * para itálico, para sublinhado.\"\r\n className=\"h-full min-h-[400px] resize-none scrollbar-thin scrollbar-thumb-blue-400 scrollbar-track-blue-100\"\r\n style={{ maxHeight: 400, overflow: 'auto' }}\r\n />\r\n
\r\n
\r\n )}\r\n\r\n {activeTab === \"imagens\" && (\r\n
\r\n
\r\n \r\n \r\n
\r\n \r\n
\r\n {imagens.map((img) => (\r\n
\r\n {img.type.startsWith('image/') ? (\r\n {img.name}\r\n\r\n ) : (\r\n
\r\n \r\n
\r\n )}\r\n

{img.name}

\r\n setImagens(prev => prev.filter(i => i.id !== img.id))}\r\n >\r\n Remover\r\n \r\n
\r\n ))}\r\n
\r\n
\r\n )}\r\n\r\n {activeTab === \"campos\" && (\r\n
\r\n
\r\n \r\n setCampos(prev => ({ ...prev, cid: e.target.value }))}\r\n placeholder=\"Ex: M25.5, I10, etc.\"\r\n />\r\n
\r\n
\r\n \r\n setCampos(prev => ({ ...prev, exame: e.target.value }))}\r\n placeholder=\"Exame realizado\"\r\n />\r\n
\r\n
\r\n \r\n setCampos(prev => ({ ...prev, diagnostico: e.target.value }))}\r\n placeholder=\"Diagnóstico principal\"\r\n rows={3}\r\n />\r\n
\r\n
\r\n \r\n setCampos(prev => ({ ...prev, conclusao: e.target.value }))}\r\n placeholder=\"Conclusão do laudo\"\r\n rows={3}\r\n />\r\n
\r\n
\r\n
\r\n setCampos(prev => ({ ...prev, mostrarData: e.target.checked }))}\r\n />\r\n \r\n
\r\n
\r\n setCampos(prev => ({ ...prev, mostrarAssinatura: e.target.checked }))}\r\n />\r\n \r\n
\r\n
\r\n {/* Assinatura Digital removida dos campos */}\r\n
\r\n )}\r\n
\r\n\r\n {/* Preview Panel */}\r\n {showPreview && (\r\n
\r\n
\r\n

Pré-visualização do Laudo

\r\n
\r\n
\r\n
\r\n {/* Header do Laudo */}\r\n
\r\n

\r\n LAUDO MÉDICO {campos.especialidade ? `- ${campos.especialidade.toUpperCase()}` : ''}\r\n

\r\n {campos.exame && (\r\n

{campos.exame}

\r\n )}\r\n {campos.cid && (\r\n

CID: {campos.cid}

\r\n )}\r\n {campos.diagnostico && (\r\n

Diagnóstico: {campos.diagnostico}

\r\n )}\r\n {campos.conclusao && (\r\n

Conclusão: {campos.conclusao}

\r\n )}\r\n {campos.mostrarData && (\r\n

\r\n Data: {new Date().toLocaleDateString('pt-BR')}\r\n

\r\n )}\r\n
\r\n\r\n {/* Dados do Paciente */}\r\n {(isNewLaudo ? pacienteSelecionado : laudo?.paciente) && (\r\n
\r\n

Dados do Paciente:

\r\n {isNewLaudo && pacienteSelecionado ? (\r\n <>\r\n

Nome: {getPatientName(pacienteSelecionado)}

\r\n

ID: {getPatientId(pacienteSelecionado)}

\r\n

CPF: {getPatientCpf(pacienteSelecionado)}

\r\n

Idade: {getPatientAge(pacienteSelecionado)} anos

\r\n

Sexo: {getPatientSex(pacienteSelecionado)}

\r\n

CID: {campos.cid || '---'}

\r\n

Diagnóstico: {campos.diagnostico || '---'}

\r\n

Conclusão: {campos.conclusao || '---'}

\r\n \r\n ) : (\r\n <>\r\n

Nome: {getPatientName(laudo?.paciente)}

\r\n

ID: {getPatientId(laudo?.paciente)}

\r\n

CPF: {getPatientCpf(laudo?.paciente)}

\r\n

Idade: {getPatientAge(laudo?.paciente)} anos

\r\n

Sexo: {getPatientSex(laudo?.paciente)}

\r\n

CID: {campos.cid || laudo?.cid || '---'}

\r\n

Diagnóstico: {campos.diagnostico || laudo?.diagnostico || '---'}

\r\n

Conclusão: {campos.conclusao || laudo?.conclusao || '---'}

\r\n \r\n )}\r\n
\r\n )}\r\n\r\n {/* Conteúdo */}\r\n
\r\n
\r\n
\r\n\r\n {/* Imagens */}\r\n {imagens.length > 0 && (\r\n
\r\n

Imagens:

\r\n
\r\n {imagens.map((img) => (\r\n {img.name}\r\n\r\n ))}\r\n
\r\n
\r\n )}\r\n\r\n {/* Assinatura Digital em tempo real */}\r\n {campos.mostrarAssinatura && (\r\n
\r\n {assinaturaImg && assinaturaImg.length > 30 ? (\r\n \"Assinatura\r\n ) : (\r\n
Assine no campo ao lado para visualizar aqui.
\r\n )}\r\n
\r\n

{((profileData as any)?.nome || (profileData as any)?.nome_social) || user?.name || 'Squad-20'}

\r\n {(((profileData as any)?.crm) || ((user?.profile as any)?.crm)) ? (\r\n // Ensure we render a single 'CRM ' prefix followed by the raw number\r\n

CRM {(((profileData as any)?.crm) || (user?.profile as any)?.crm).toString().replace(/^(?:CRM\\s*)+/i, '').trim()}

\r\n ) : null}\r\n
\r\n )}\r\n
\r\n
\r\n
\r\n )}\r\n
\r\n\r\n {/* Footer */}\r\n
\r\n
\r\n
\r\n Este editor permite escrever relatórios de forma livre, com formatação de texto rica.\r\n
\r\n
\r\n \r\n {/* botão 'Salvar Rascunho' removido por não ser utilizado */}\r\n {\r\n try {\r\n const userId = user?.id || '00000000-0000-0000-0000-000000000001';\r\n // compor due_at a partir dos campos de data/hora, se fornecidos\r\n let composedDueAt = undefined;\r\n if (prazoDate) {\r\n // if time not provided, default to 23:59\r\n const t = prazoTime || '23:59';\r\n composedDueAt = new Date(`${prazoDate}T${t}:00`).toISOString();\r\n }\r\n\r\n const payload = {\r\n patient_id: pacienteSelecionado?.id,\r\n order_number: '',\r\n exam: campos.exame || '',\r\n diagnosis: campos.diagnostico || '',\r\n conclusion: campos.conclusao || '',\r\n cid_code: campos.cid || '',\r\n content_html: content,\r\n content_json: {},\r\n // status intentionally omitted — não enviar 'draft'\r\n requested_by: solicitanteId || userId,\r\n due_at: composedDueAt ?? new Date().toISOString(),\r\n hide_date: !campos.mostrarData,\r\n hide_signature: !campos.mostrarAssinatura,\r\n };\r\n\r\n if (isNewLaudo) {\r\n if (createNewReport) {\r\n const created = await createNewReport(payload as any);\r\n if (onSaved) onSaved(created);\r\n }\r\n } else {\r\n // Atualizar laudo existente: confirmar e enviar apenas diff\r\n const targetId = laudo?.id ?? laudo?.order_number ?? null;\r\n if (!targetId) throw new Error('ID do laudo ausente, não é possível atualizar');\r\n\r\n // Montar objeto contendo somente campos alterados\r\n const original = laudo || {};\r\n const candidate: any = {\r\n patient_id: payload.patient_id,\r\n order_number: payload.order_number,\r\n exam: payload.exam,\r\n diagnosis: payload.diagnosis,\r\n conclusion: payload.conclusion,\r\n cid_code: payload.cid_code,\r\n content_html: payload.content_html,\r\n // content_json intentionally left as full replacement if changed\r\n // status omitted on purpose\r\n requested_by: payload.requested_by,\r\n due_at: payload.due_at,\r\n hide_date: payload.hide_date,\r\n hide_signature: payload.hide_signature,\r\n };\r\n\r\n const diff: any = {};\r\n for (const k of Object.keys(candidate)) {\r\n const val = candidate[k];\r\n const origVal = original[k];\r\n // Considerar string/undefined equivalence\r\n if (typeof val === 'string') {\r\n if ((origVal ?? '') !== (val ?? '')) diff[k] = val;\r\n } else if (typeof val === 'boolean') {\r\n if (origVal !== val) diff[k] = val;\r\n } else if (val !== undefined && val !== null) {\r\n if (JSON.stringify(origVal) !== JSON.stringify(val)) diff[k] = val;\r\n }\r\n }\r\n\r\n if (Object.keys(diff).length === 0) {\r\n toast({ title: 'Nada a atualizar', description: 'Nenhuma alteração detectada.', variant: 'default' });\r\n } else {\r\n const ok = window.confirm('Deseja realmente atualizar este laudo? As alterações serão enviadas ao servidor.');\r\n if (!ok) return;\r\n if (updateExistingReport) {\r\n const updated = await updateExistingReport(String(targetId), diff as any);\r\n if (onSaved) onSaved(updated);\r\n }\r\n }\r\n }\r\n\r\n if (reloadReports) {\r\n await reloadReports();\r\n }\r\n\r\n toast({\r\n title: isNewLaudo ? 'Laudo criado com sucesso!' : 'Laudo atualizado com sucesso!',\r\n description: isNewLaudo ? 'O laudo foi liberado e salvo.' : 'As alterações foram salvas.',\r\n variant: 'default',\r\n });\r\n onClose();\r\n } catch (err) {\r\n toast({\r\n title: isNewLaudo ? 'Erro ao criar laudo' : 'Erro ao atualizar laudo',\r\n description: (err && typeof err === 'object' && 'message' in err) ? (err as any).message : String(err) || 'Tente novamente.',\r\n variant: 'destructive',\r\n });\r\n }\r\n }}\r\n >\r\n {isNewLaudo ? \"Liberar Laudo\" : \"Atualizar Laudo\"}\r\n \r\n
\r\n
\r\n
\r\n
\r\n
\r\n );\r\n }\r\n\r\n \r\n const renderComunicacaoSection = () => (\r\n
\r\n

Comunicação com o Paciente

\r\n
\r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n
\r\n \r\n

03/09/2025

\r\n
\r\n
\r\n \r\n

Pendente

\r\n
\r\n
\r\n
\r\n \r\n
\r\n

\"Ok, obrigado pelo lembrete!\"

\r\n

03/09/2025 14:30

\r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n );\r\n\r\n \r\n const renderPerfilSection = () => (\r\n
\r\n
\r\n

Meu Perfil

\r\n {!isEditingProfile ? (\r\n \r\n ) : (\r\n
\r\n \r\n \r\n
\r\n )}\r\n
\r\n\r\n
\r\n {/* Informações Pessoais */}\r\n
\r\n

Informações Pessoais

\r\n \r\n
\r\n \r\n

{profileData.nome}

\r\n Este campo não pode ser alterado\r\n
\r\n\r\n
\r\n \r\n {isEditingProfile ? (\r\n handleProfileChange('email', e.target.value)}\r\n />\r\n ) : (\r\n

{profileData.email}

\r\n )}\r\n
\r\n\r\n
\r\n \r\n {isEditingProfile ? (\r\n handleProfileChange('telefone', e.target.value)}\r\n />\r\n ) : (\r\n

{profileData.telefone}

\r\n )}\r\n
\r\n\r\n
\r\n \r\n

{profileData.crm}

\r\n Este campo não pode ser alterado\r\n
\r\n\r\n
\r\n \r\n {isEditingProfile ? (\r\n handleProfileChange('especialidade', e.target.value)}\r\n />\r\n ) : (\r\n

{profileData.especialidade}

\r\n )}\r\n
\r\n
\r\n\r\n {/* Endereço e Contato */}\r\n
\r\n

Endereço e Contato

\r\n \r\n
\r\n \r\n {isEditingProfile ? (\r\n handleProfileChange('endereco', e.target.value)}\r\n />\r\n ) : (\r\n

{profileData.endereco}

\r\n )}\r\n
\r\n\r\n
\r\n \r\n {isEditingProfile ? (\r\n handleProfileChange('cidade', e.target.value)}\r\n />\r\n ) : (\r\n

{profileData.cidade}

\r\n )}\r\n
\r\n\r\n
\r\n \r\n {isEditingProfile ? (\r\n handleProfileChange('cep', e.target.value)}\r\n />\r\n ) : (\r\n

{profileData.cep}

\r\n )}\r\n
\r\n\r\n {/* Biografia removida: não é um campo no registro de médico */}\r\n
\r\n
\r\n\r\n {/* Foto do Perfil */}\r\n
\r\n

Foto do Perfil

\r\n
\r\n \r\n \r\n {profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()}\r\n \r\n \r\n {isEditingProfile && (\r\n
\r\n \r\n

\r\n Formatos aceitos: JPG, PNG (máx. 2MB)\r\n

\r\n
\r\n )}\r\n
\r\n
\r\n
\r\n );\r\n\r\n \r\n const renderActiveSection = () => {\r\n switch (activeSection) {\r\n case 'calendario':\r\n return renderCalendarioSection();\r\n case 'pacientes':\r\n return (\r\n
\r\n

Pacientes

\r\n
\r\n \r\n \r\n \r\n Nome\r\n CPF\r\n Idade\r\n \r\n \r\n \r\n {pacientes.map((paciente) => (\r\n \r\n {paciente.nome}\r\n {paciente.cpf}\r\n {getPatientAge(paciente) ? `${getPatientAge(paciente)} anos` : '-'}\r\n \r\n ))}\r\n \r\n
\r\n
\r\n
\r\n );\r\n case 'laudos':\r\n return renderLaudosSection();\r\n case 'comunicacao':\r\n return renderComunicacaoSection();\r\n case 'perfil':\r\n return renderPerfilSection();\r\n default:\r\n return renderCalendarioSection();\r\n }\r\n };\r\n\r\n return (\r\n \r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n \r\n \r\n \r\n
\r\n

Conta do profissional

\r\n

{profileData.nome}

\r\n

{(profileData.crm ? `CRM: ${profileData.crm}` : '') + (profileData.especialidade ? ` • ${profileData.especialidade}` : '')}

\r\n {user?.email && (\r\n

Logado como: {user.email}

\r\n )}\r\n
\r\n
\r\n
\r\n \r\n \r\n \r\n
\r\n
\r\n \r\n
\r\n {}\r\n \r\n\r\n
\r\n
\r\n

Área do Profissional de Saúde

\r\n
\r\n

Bem-vindo à sua área exclusiva.

\r\n\r\n {renderActiveSection()}\r\n
\r\n
\r\n\r\n {}\r\n {showPopup && (\r\n
\r\n\r\n
\r\n\r\n {step === 1 && (\r\n <>\r\n

Selecionar Paciente

\r\n

\r\n Data: {selectedDate ? new Date(selectedDate + 'T00:00:00').toLocaleDateString('pt-BR') : 'Não selecionada'}\r\n

\r\n setNewEvent({ ...newEvent, title: value })}\r\n >\r\n \r\n \r\n \r\n \r\n {pacientes && pacientes.map((paciente) => (\r\n \r\n {paciente.nome} - {paciente.cpf}\r\n \r\n ))}\r\n \r\n \r\n
\r\n setShowPopup(false)}\r\n variant=\"outline\"\r\n className=\"flex-1 hover:bg-blue-50 dark:hover:bg-accent dark:hover:text-accent-foreground\"\r\n >\r\n Cancelar\r\n \r\n \r\n Próximo\r\n \r\n
\r\n \r\n )}\r\n\r\n {step === 2 && (\r\n <>\r\n

Tipo da Consulta

\r\n setNewEvent({ ...newEvent, type: value })}\r\n >\r\n \r\n \r\n \r\n \r\n {Object.keys(colorsByType).map((type) => (\r\n \r\n {type}\r\n \r\n ))}\r\n \r\n \r\n
\r\n setStep(1)}\r\n variant=\"outline\"\r\n className=\"flex-1\"\r\n >\r\n Voltar\r\n \r\n \r\n Próximo\r\n \r\n
\r\n \r\n )}\r\n\r\n {step === 3 && (\r\n <>\r\n

Horário da Consulta

\r\n setNewEvent({ ...newEvent, time: e.target.value })}\r\n className=\"mb-4\"\r\n />\r\n
\r\n setStep(2)}\r\n variant=\"outline\"\r\n className=\"flex-1\"\r\n >\r\n Voltar\r\n \r\n \r\n {editingEvent ? \"Salvar\" : \"Agendar\"}\r\n \r\n
\r\n \r\n )}\r\n
\r\n
\r\n )}\r\n\r\n {}\r\n {showActionModal && selectedEvent && (\r\n
\r\n
\r\n

\r\n Consulta de {selectedEvent.title}\r\n

\r\n

\r\n {selectedEvent.extendedProps.type} às {selectedEvent.extendedProps.time}\r\n

\r\n\r\n
\r\n \r\n \r\n Editar\r\n \r\n \r\n \r\n Excluir\r\n \r\n
\r\n\r\n setShowActionModal(false)}\r\n variant=\"outline\"\r\n className=\"w-full mt-2 hover:bg-blue-50 dark:hover:bg-accent dark:hover:text-accent-foreground\"\r\n >\r\n Cancelar\r\n \r\n
\r\n
\r\n )}\r\n
\r\n
\r\n );\r\n};\r\n\r\nconst getShortId = (id?: string) => {\r\n if (!id) return '-';\r\n try {\r\n return id.length > 10 ? `${id.slice(0, 8)}...` : id;\r\n } catch (e) {\r\n return id;\r\n }\r\n};\r\n\r\nexport default ProfissionalPage;","usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\resultados\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\app\\sobre\\page.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ProtectedRoute.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\about-section.tsx","messages":[{"ruleId":"@next/next/no-img-element","severity":1,"message":"Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element","line":15,"column":15,"nodeType":"JSXOpeningElement","endLine":19,"endColumn":17}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { Card } from \"@/components/ui/card\"\r\nimport { Lightbulb, CheckCircle } from \"lucide-react\"\r\n\r\nexport function AboutSection() {\r\n const values = [\"Inovação\", \"Segurança\", \"Discrição\", \"Transparência\", \"Agilidade\"]\r\n\r\n return (\r\n
\r\n
\r\n
\r\n {}\r\n
\r\n {}\r\n
\r\n \r\n
\r\n\r\n {}\r\n \r\n
\r\n
\r\n \r\n
\r\n
\r\n

NOSSO OBJETIVO

\r\n

\r\n Nosso compromisso é garantir qualidade, segurança e sigilo em cada atendimento, unindo tecnologia à\r\n responsabilidade médica.\r\n

\r\n
\r\n
\r\n
\r\n
\r\n\r\n {}\r\n
\r\n
\r\n
\r\n SOBRE NÓS\r\n
\r\n

\r\n Experimente o futuro do gerenciamento dos seus atendimentos médicos\r\n

\r\n
\r\n\r\n
\r\n

\r\n Somos uma plataforma inovadora que conecta pacientes e médicos de forma prática, segura e humanizada.\r\n Nosso objetivo é simplificar o processo de emissão e acompanhamento de laudos médicos, oferecendo um\r\n ambiente online confiável e acessível.\r\n

\r\n

\r\n Aqui, os pacientes podem registrar suas informações de saúde e solicitar laudos de forma rápida,\r\n enquanto os médicos têm acesso a ferramentas que facilitam a análise, validação e emissão dos\r\n documentos.\r\n

\r\n
\r\n\r\n
\r\n

Nossos valores

\r\n
\r\n {values.map((value, index) => (\r\n
\r\n \r\n {value}\r\n
\r\n ))}\r\n
\r\n
\r\n
\r\n
\r\n
\r\n
\r\n )\r\n}\r\n","usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\admin\\AssignmentForm.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\agenda\\FooterAgenda.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\agenda\\HeaderAgenda.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\agendamento\\AgendaCalendar.tsx","messages":[{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":185,"column":22,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: ‘C' para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: ’C' para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `’`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":185,"column":24,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C‘ para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C’ para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `’`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":185,"column":43,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, ‘F' para fila de espera\r\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, ’F' para fila de espera\r\n "},"desc":"Replace with `’`."}]},{"ruleId":"react/no-unescaped-entities","severity":2,"message":"`'` can be escaped with `'`, `‘`, `'`, `’`.","line":185,"column":45,"nodeType":"JSXText","messageId":"unescapedEntityAlts","suggestions":[{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"‘"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, 'F‘ para fila de espera\r\n "},"desc":"Replace with `‘`."},{"messageId":"replaceWithAlt","data":{"alt":"'"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, 'F' para fila de espera\r\n "},"desc":"Replace with `'`."},{"messageId":"replaceWithAlt","data":{"alt":"’"},"fix":{"range":[6609,6688],"text":"\r\n Atalhos: 'C' para calendário, 'F’ para fila de espera\r\n "},"desc":"Replace with `’`."}]}],"suppressedMessages":[],"errorCount":4,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":"\r\n'use client';\r\n\r\nimport { useState } from 'react';\r\nimport { ChevronLeft, ChevronRight, Plus, Clock, User, Calendar as CalendarIcon } from 'lucide-react';\r\n\r\ninterface Appointment {\r\n id: string;\r\n patient: string;\r\n time: string;\r\n duration: number;\r\n type: 'consulta' | 'exame' | 'retorno';\r\n status: 'confirmed' | 'pending' | 'absent';\r\n professional: string;\r\n notes: string;\r\n}\r\n\r\ninterface Professional {\r\n id: string;\r\n name: string;\r\n specialty: string;\r\n}\r\n\r\ninterface AgendaCalendarProps {\r\n professionals: Professional[];\r\n appointments: Appointment[];\r\n onAddAppointment: () => void;\r\n onEditAppointment: (appointment: Appointment) => void;\r\n}\r\n\r\nexport default function AgendaCalendar({ \r\n professionals, \r\n appointments, \r\n onAddAppointment, \r\n onEditAppointment \r\n}: AgendaCalendarProps) {\r\n const [view, setView] = useState<'day' | 'week' | 'month'>('week');\r\n const [selectedProfessional, setSelectedProfessional] = useState('all');\r\n const [currentDate, setCurrentDate] = useState(new Date());\r\n\r\n const timeSlots = Array.from({ length: 11 }, (_, i) => {\r\n const hour = i + 8; // Das 8h às 18h\r\n return [`${hour.toString().padStart(2, '0')}:00`, `${hour.toString().padStart(2, '0')}:30`];\r\n }).flat();\r\n\r\n const getStatusColor = (status: string) => {\r\n switch (status) {\r\n case 'confirmed': return 'bg-green-100 border-green-500 text-green-800';\r\n case 'pending': return 'bg-yellow-100 border-yellow-500 text-yellow-800';\r\n case 'absent': return 'bg-red-100 border-red-500 text-red-800';\r\n default: return 'bg-gray-100 border-gray-500 text-gray-800';\r\n }\r\n };\r\n\r\n const getTypeIcon = (type: string) => {\r\n switch (type) {\r\n case 'consulta': return '🩺';\r\n case 'exame': return '📋';\r\n case 'retorno': return '↩️';\r\n default: return '📅';\r\n }\r\n };\r\n\r\n const formatDate = (date: Date) => {\r\n return date.toLocaleDateString('pt-BR', { \r\n weekday: 'long', \r\n day: 'numeric', \r\n month: 'long', \r\n year: 'numeric' \r\n });\r\n };\r\n\r\n const navigateDate = (direction: 'prev' | 'next') => {\r\n const newDate = new Date(currentDate);\r\n if (view === 'day') {\r\n newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));\r\n } else if (view === 'week') {\r\n newDate.setDate(newDate.getDate() + (direction === 'next' ? 7 : -7));\r\n } else {\r\n newDate.setMonth(newDate.getMonth() + (direction === 'next' ? 1 : -1));\r\n }\r\n setCurrentDate(newDate);\r\n };\r\n\r\n const goToToday = () => {\r\n setCurrentDate(new Date());\r\n };\r\n\r\n \r\n const filteredAppointments = selectedProfessional === 'all' \r\n ? appointments \r\n : appointments.filter(app => app.professional === selectedProfessional);\r\n\r\n return (\r\n
\r\n
\r\n
\r\n

Agenda

\r\n \r\n
\r\n \r\n \r\n
\r\n setView('day')}\r\n className={`px-3 py-2 text-sm font-medium rounded-l-md ${\r\n view === 'day' \r\n ? 'bg-blue-100 text-blue-700 border border-blue-300' \r\n : 'bg-white text-gray-700 border border-gray-300'\r\n }`}\r\n >\r\n Dia\r\n \r\n setView('week')}\r\n className={`px-3 py-2 text-sm font-medium -ml-px ${\r\n view === 'week' \r\n ? 'bg-blue-100 text-blue-700 border border-blue-300' \r\n : 'bg-white text-gray-700 border border-gray-300'\r\n }`}\r\n >\r\n Semana\r\n \r\n setView('month')}\r\n className={`px-3 py-2 text-sm font-medium -ml-px rounded-r-md ${\r\n view === 'month' \r\n ? 'bg-blue-100 text-blue-700 border border-blue-300' \r\n : 'bg-white text-gray-700 border border-gray-300'\r\n }`}\r\n >\r\n Mês\r\n \r\n
\r\n \r\n \r\n
\r\n
\r\n
\r\n\r\n
\r\n
\r\n
\r\n \r\n

\r\n {formatDate(currentDate)}\r\n

\r\n \r\n \r\n
\r\n
\r\n Atalhos: 'C' para calendário, 'F' para fila de espera\r\n
\r\n
\r\n
\r\n\r\n {}\r\n {view !== 'month' && (\r\n
\r\n
\r\n
\r\n
\r\n
\r\n Hora\r\n
\r\n {timeSlots.map(time => (\r\n
\r\n {time}\r\n
\r\n ))}\r\n
\r\n \r\n
\r\n
\r\n {currentDate.toLocaleDateString('pt-BR', { weekday: 'long' })}\r\n
\r\n
\r\n {timeSlots.map(time => (\r\n
\r\n ))}\r\n \r\n {filteredAppointments.map(app => {\r\n const [date, timeStr] = app.time.split('T');\r\n const [hours, minutes] = timeStr.split(':');\r\n const hour = parseInt(hours);\r\n const minute = parseInt(minutes);\r\n \r\n return (\r\n onEditAppointment(app)}\r\n >\r\n
\r\n
\r\n
\r\n \r\n {app.patient}\r\n
\r\n
\r\n \r\n {hours}:{minutes} - {app.type} {getTypeIcon(app.type)}\r\n
\r\n
\r\n {professionals.find(p => p.id === app.professional)?.name}\r\n
\r\n
\r\n
\r\n {app.status === 'confirmed' ? 'confirmado' : app.status === 'pending' ? 'pendente' : 'ausente'}\r\n
\r\n
\r\n
\r\n );\r\n })}\r\n
\r\n
\r\n
\r\n
\r\n
\r\n )}\r\n\r\n {}\r\n {view === 'month' && (\r\n
\r\n
\r\n {filteredAppointments.map(app => {\r\n const [date, timeStr] = app.time.split('T');\r\n const [hours, minutes] = timeStr.split(':');\r\n \r\n return (\r\n
\r\n
\r\n
\r\n \r\n {app.patient}\r\n
\r\n
\r\n \r\n {hours}:{minutes} - {app.type} {getTypeIcon(app.type)}\r\n
\r\n
\r\n {professionals.find(p => p.id === app.professional)?.name}\r\n
\r\n
\r\n {app.notes && (\r\n
\r\n {app.notes}\r\n
\r\n )}\r\n
\r\n \r\n
\r\n
\r\n );\r\n })}\r\n
\r\n
\r\n )}\r\n
\r\n );\r\n}","usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\agendamento\\AppointmentModal.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\agendamento\\ListaEspera.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\agendamento\\index.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\credentials-dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\dashboard\\header.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\dashboard\\sidebar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\footer.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\forms\\calendar-registration-form.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\forms\\doctor-registration-form.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\forms\\patient-registration-form.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\header.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\hero-section.tsx","messages":[{"ruleId":"@next/next/no-img-element","severity":1,"message":"Using `` could result in slower LCP and higher bandwidth. Consider using `` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element","line":49,"column":15,"nodeType":"JSXOpeningElement","endLine":53,"endColumn":17}],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":"import { Button } from \"@/components/ui/button\"\r\nimport { Shield, Clock, Users } from \"lucide-react\"\r\nimport Link from \"next/link\"\r\n\r\nexport function HeroSection() {\r\n return (\r\n
\r\n
\r\n
\r\n {}\r\n
\r\n
\r\n
\r\n APROXIMANDO MÉDICOS E PACIENTES\r\n
\r\n

\r\n Segurança, Confiabilidade e{\" \"}\r\n Rapidez\r\n

\r\n
\r\n

Experimente o futuro dos agendamentos.

\r\n

Encontre profissionais capacitados e marque já sua consulta.

\r\n
\r\n
\r\n\r\n {}\r\n
\r\n \r\n \r\n Sou Profissional de Saúde\r\n \r\n
\r\n
\r\n\r\n {}\r\n
\r\n
\r\n \r\n
\r\n
\r\n
\r\n\r\n {}\r\n
\r\n
\r\n
\r\n \r\n
\r\n
\r\n

Laudos digitais e padronizados

\r\n
\r\n
\r\n\r\n
\r\n
\r\n \r\n
\r\n
\r\n

Notificações automáticas ao paciente

\r\n
\r\n
\r\n\r\n
\r\n
\r\n \r\n
\r\n
\r\n

LGPD: controle de acesso e consentimento

\r\n
\r\n
\r\n
\r\n
\r\n
\r\n )\r\n}\r\n","usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\simple-theme-toggle.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\theme-provider.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\theme-toggle.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\accordion.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\alert-dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\alert.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\aspect-ratio.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\avatar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\badge.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\breadcrumb.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\button.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\calendar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\carousel.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\chart.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\checkbox.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\collapsible.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\command.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\context-menu.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\dialog.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\drawer.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\dropdown-menu.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\form.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\hover-card.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\input-otp.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\input.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\label.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\menubar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\navigation-menu.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\pagination.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\popover.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\progress.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\radio-group.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\resizable.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\scroll-area.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\select.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\separator.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\sheet.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\sidebar.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\skeleton.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\slider.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\sonner.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\switch.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\table.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\tabs.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\textarea.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\toast.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\toaster.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\toggle-group.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\toggle.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\tooltip.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\use-mobile.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\components\\ui\\use-toast.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\eslint.config.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\hooks\\UseAgenda.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\hooks\\use-force-default-theme.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\hooks\\use-mobile.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\hooks\\use-toast.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\hooks\\useAuth.tsx","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\hooks\\useReports.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\api.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\assignment.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\auth.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\debug-utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\env-config.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\http.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\jwt.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\mocks\\appointment-mocks.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\reports.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\lib\\utils.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\next.config.mjs","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\postcss.config.mjs","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\public\\forward-client-logs.js","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\src\\app\\api\\assign-role\\route.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\types\\auth.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\types\\react-signature-canvas.d.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]},{"filePath":"C:\\Users\\Gabri\\Desktop\\riseup-squad20\\susconecta\\types\\report-types.ts","messages":[],"suppressedMessages":[],"errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":[]}] \ No newline at end of file diff --git a/susconecta/hooks/useAuth.tsx b/susconecta/hooks/useAuth.tsx index 488c318..d577108 100644 --- a/susconecta/hooks/useAuth.tsx +++ b/susconecta/hooks/useAuth.tsx @@ -106,31 +106,15 @@ export function AuthProvider({ children }: { children: ReactNode }) { if (isExpired(storedToken)) { console.log('[AUTH] Token expirado - tentando renovar...') await new Promise(resolve => setTimeout(resolve, 1000)) - + const refreshToken = localStorage.getItem(AUTH_STORAGE_KEYS.REFRESH_TOKEN) if (refreshToken && !isExpired(refreshToken)) { - // Tentar renovar via HTTP client (que já tem a lógica) - try { - await httpClient.get('/auth/v1/me') // Trigger refresh se necessário - - // Se chegou aqui, refresh foi bem-sucedido - const newToken = localStorage.getItem(AUTH_STORAGE_KEYS.TOKEN) - const userData = JSON.parse(storedUser) as UserData - - if (newToken && newToken !== storedToken) { - setToken(newToken) - setUser(userData) - setAuthStatus('authenticated') - console.log('[AUTH] Token RENOVADO automaticamente!') - await new Promise(resolve => setTimeout(resolve, 800)) - return - } - } catch (refreshError) { - console.log(' [AUTH] Falha no refresh automático') - await new Promise(resolve => setTimeout(resolve, 400)) - } + // Refresh automático foi desativado no cliente. A renovação de tokens + // deve ser feita via backend seguro. Se desejar habilitar refresh no + // cliente, implemente um flow server-side ou use o SDK autenticado. + console.log('[AUTH] Refresh token presente, mas refresh automático desativado no cliente') } - + clearAuthData() return } @@ -138,38 +122,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { // Restaurar sessão válida const userData = JSON.parse(storedUser) as UserData setToken(storedToken) - // Tentar buscar profile consolidado (user-info) e mesclar - try { - const info = await getUserInfo() - if (info?.profile) { - const mapped = { - cpf: (info.profile as any).cpf ?? userData.profile?.cpf, - crm: (info.profile as any).crm ?? userData.profile?.crm, - telefone: info.profile.phone ?? userData.profile?.telefone, - foto_url: info.profile.avatar_url ?? userData.profile?.foto_url, - } - if (userData.profile) { - userData.profile = { ...userData.profile, ...mapped } - } else { - userData.profile = mapped - } - // Persistir o usuário atualizado no localStorage para evitar - // que 'auth_user.profile' fique vazio após um reload completo - try { - if (typeof window !== 'undefined') { - // Persistir também roles se disponíveis - if (info?.roles && Array.isArray(info.roles)) { - userData.roles = info.roles - } - localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(userData)) - } - } catch (e) { - console.warn('[AUTH] Falha ao persistir user (profile) no localStorage:', e) - } - } - } catch (err) { - console.warn('[AUTH] Falha ao buscar user-info na restauração de sessão:', err) - } + // Nota: chamadas a user-info foram removidas do cliente. Mantemos os dados do + // usuário que já estavam persistidos localmente sem tentar reconciliar com funções remotas. setUser(userData) setAuthStatus('authenticated') @@ -197,74 +151,57 @@ export function AuthProvider({ children }: { children: ReactNode }) { try { console.log('[AUTH] Iniciando login:', { email, userType }) - const response = await loginUser(email, password, userType) + const response = await loginUser(email, password) - // Após receber token, buscar roles/permissions reais e reconciliar userType - try { - const infoRes = await fetch(`${ENV_CONFIG.SUPABASE_URL}/functions/v1/user-info`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - 'Authorization': `Bearer ${response.access_token}`, - 'apikey': ENV_CONFIG.SUPABASE_ANON_KEY, - } - }) - - if (infoRes.ok) { - const info = await infoRes.json().catch(() => null) - const roles: string[] = Array.isArray(info?.roles) ? info.roles : (info?.roles ? [info.roles] : []) - - // Derivar tipo de usuário a partir dos roles - let derived: UserType = 'paciente' - if (roles.includes('admin') || roles.includes('gestor') || roles.includes('secretaria')) { - derived = 'administrador' - } else if (roles.includes('medico') || roles.includes('enfermeiro')) { - derived = 'profissional' - } - - // Atualizar userType caso seja diferente - if (response.user && response.user.userType !== derived) { - response.user.userType = derived - console.log('[AUTH] userType reconciled from roles ->', derived) - } - // Persistir roles no objeto user para suportar multi-role - if (response.user && roles.length) { - response.user.roles = roles - } - } else { - console.warn('[AUTH] Falha ao obter user-info para reconciliar roles:', infoRes.status) - } - } catch (err) { - console.warn('[AUTH] Erro ao buscar user-info após login (não crítico):', err) - } - - // Após login, tentar buscar profile consolidado e mesclar antes de persistir - try { - const info = await getUserInfo() - if (info?.profile && response.user) { - const mapped = { - cpf: (info.profile as any).cpf ?? response.user.profile?.cpf, - crm: (info.profile as any).crm ?? response.user.profile?.crm, - telefone: info.profile.phone ?? response.user.profile?.telefone, - foto_url: info.profile.avatar_url ?? response.user.profile?.foto_url, - } - if (response.user.profile) { - response.user.profile = { ...response.user.profile, ...mapped } - } else { - response.user.profile = mapped - } - } - } catch (err) { - console.warn('[AUTH] Falha ao buscar user-info após login (não crítico):', err) - } + // Nota: busca de user-info e reconciliação via funções server-side foi removida do cliente. + // Mantemos o objeto response.user retornado pelo fluxo de autenticação sem tentativas + // adicionais de mesclagem aqui. + // Salva dados iniciais (token + user retornado pelo login) saveAuthData( response.access_token, response.user, response.refresh_token ) + // Tentar buscar informações consolidadas do usuário (profile + roles) + try { + const userInfo = await getUserInfo() + if (userInfo) { + const mergedUser: any = { + ...response.user, + // prefer profile and roles vindos do userInfo + profile: userInfo.profile ?? (response.user as any).profile, + roles: userInfo.roles ?? (response.user as any).roles ?? [], + // garantir id/email caso estejam no userInfo + id: (response.user as any).id || (userInfo.user as any)?.id, + email: (response.user as any).email || (userInfo.user as any)?.email, + } + + // Inferir userType a partir de roles se estiver ausente + if (!mergedUser.userType || mergedUser.userType === undefined) { + const r: string[] = mergedUser.roles || []; + if (r.includes('admin') || r.includes('gestor') || r.includes('secretaria')) mergedUser.userType = 'administrador' + else if (r.includes('medico') || r.includes('enfermeiro')) mergedUser.userType = 'profissional' + else if (r.includes('paciente')) mergedUser.userType = 'paciente' + } + + // Persistir e atualizar estado com dados consolidados + try { + if (typeof window !== 'undefined') { + localStorage.setItem(AUTH_STORAGE_KEYS.USER, JSON.stringify(mergedUser)) + localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, mergedUser.userType || '') + } + setUser(mergedUser as any) + console.log('[AUTH] userInfo consolidado e salvo:', { email: mergedUser.email, roles: mergedUser.roles }) + } catch (e) { + console.warn('[AUTH] Não foi possível persistir userInfo consolidado:', e) + } + } + } catch (infoErr) { + console.warn('[AUTH] getUserInfo falhou (ignorar no cliente):', infoErr) + } + console.log('[AUTH] Login realizado com sucesso') return true diff --git a/susconecta/lib/api.ts b/susconecta/lib/api.ts index 954589f..b828103 100644 --- a/susconecta/lib/api.ts +++ b/susconecta/lib/api.ts @@ -1,6 +1,7 @@ // lib/api.ts import { ENV_CONFIG } from '@/lib/env-config'; +import { FUNCTIONS_ENDPOINTS } from '@/lib/config'; // Use ENV_CONFIG for SUPABASE URL and anon key in frontend export type ApiOk = { @@ -613,17 +614,9 @@ export async function excluirPaciente(id: string | number): Promise { * Este endpoint usa a service role key e valida se o requisitante é administrador. */ export async function assignRoleServerSide(userId: string, role: string): Promise { - const url = `/api/assign-role`; - const token = getAuthToken(); - const res = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - body: JSON.stringify({ user_id: userId, role }), - }); - return await parse(res); + // Operação administrativa removida do cliente. + // Atribuição de roles é feita exclusivamente via backend com a service role key. + throw new TypeError('assignRoleServerSide removido do cliente. Use APIs administrativas no servidor para atribuir roles.'); } // ===== PACIENTES (Extra: verificação de CPF duplicado) ===== export async function verificarCpfDuplicado(cpf: string): Promise { @@ -935,6 +928,15 @@ export async function vincularUserIdMedico(medicoId: string | number, userId: st * Retorna o paciente atualizado. */ export async function vincularUserIdPaciente(pacienteId: string | number, userId: string): Promise { + // Nota: a coluna `user_id` não existe no schema público de `patients`. + // Operações de vinculação devem ser feitas server-side usando a service role key. + // Esta função NÃO deve ser chamada a partir do browser. Se for chamada no cliente, + // lançamos um erro que instrui a executar a operação via backend/rota administrativa. + if (typeof window !== 'undefined') { + throw new TypeError('Operação inválida: vincularUserIdPaciente não pode ser executada no cliente. Execute a vinculação no backend com a service role key.'); + } + + // Executando no servidor (node): tentar patch direto (caso o schema permita). const url = `${REST}/patients?id=eq.${encodeURIComponent(String(pacienteId))}`; const payload = { user_id: String(userId) }; const res = await fetch(url, { @@ -1024,11 +1026,15 @@ export type UserRole = { created_at: string; }; +/** + * Listar roles de usuários + * GET /rest/v1/user_roles + */ export async function listarUserRoles(): Promise { - const url = `${API_BASE}/rest/v1/user_roles`; - const res = await fetch(url, { - method: "GET", - headers: baseHeaders(), + const url = `${REST}/user_roles`; + const res = await fetch(url, { + method: 'GET', + headers: baseHeaders() }); return await parse(res); } @@ -1096,21 +1102,47 @@ export type UserInfo = { permissions: Permissions; }; +/** + * Obter dados do usuário atual via Supabase Auth + * GET /auth/v1/user + */ export async function getCurrentUser(): Promise { - const url = `${API_BASE}/auth/v1/user`; + const token = getAuthToken(); + if (!token) { + throw new Error('Token não encontrado. Faça login primeiro.'); + } + + const url = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/user`; const res = await fetch(url, { - method: "GET", - headers: baseHeaders(), + method: 'GET', + headers: { + ...baseHeaders(), + 'Authorization': `Bearer ${token}`, + }, }); + return await parse(res); } +/** + * Obter informações completas do usuário + * GET /functions/v1/user-info + */ export async function getUserInfo(): Promise { - const url = `${API_BASE}/functions/v1/user-info`; + const token = getAuthToken(); + if (!token) { + throw new Error('Token não encontrado. Faça login primeiro.'); + } + + const url = FUNCTIONS_ENDPOINTS.USER_INFO; const res = await fetch(url, { - method: "GET", - headers: baseHeaders(), + method: 'GET', + headers: { + ...baseHeaders(), + 'Authorization': `Bearer ${token}`, + }, }); + return await parse(res); } @@ -1150,391 +1182,52 @@ export function gerarSenhaAleatoria(): string { return `senha${num1}${num2}${num3}!`; } +/** + * Criar novo usuário + * POST /functions/v1/create-user + */ export async function criarUsuario(input: CreateUserInput): Promise { - const url = `${API_BASE}/functions/v1/create-user`; - const res = await fetch(url, { - method: "POST", - headers: { ...baseHeaders(), "Content-Type": "application/json" }, - body: JSON.stringify(input), - }); - return await parse(res); -} - -// ===== ALTERNATIVA: Criar usuário diretamente via Supabase Auth ===== -// Esta função é um fallback caso a função server-side create-user falhe - -export async function criarUsuarioDirectAuth(input: { - email: string; - password: string; - full_name: string; - phone?: string | null; - role: UserRoleEnum; - userType?: 'profissional' | 'paciente'; -}): Promise { - console.log('[DIRECT AUTH] Criando usuário diretamente via Supabase Auth...'); - - const signupUrl = `${API_BASE}/auth/v1/signup`; - - const payload = { - email: input.email, - password: input.password, - data: { - userType: input.userType || (input.role === 'medico' ? 'profissional' : 'paciente'), - full_name: input.full_name, - phone: input.phone || '', - } - }; + const token = getAuthToken(); + if (!token) { + throw new Error('Token não encontrado. Faça login primeiro.'); + } + const url = FUNCTIONS_ENDPOINTS.CREATE_USER; + // Log do request para debugging (visível no console do navegador) try { - const response = await fetch(signupUrl, { - method: "POST", + console.debug('[USER API] POST', url, 'body:', input); + } catch (e) { + // ignore logging errors + } + + let response: Response; + try { + response = await fetch(url, { + method: 'POST', headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "apikey": ENV_CONFIG.SUPABASE_ANON_KEY, + ...baseHeaders(), + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}`, }, - body: JSON.stringify(payload), + body: JSON.stringify(input), }); - - if (!response.ok) { - const errorText = await response.text(); - let errorMsg = `Erro ao criar usuário (${response.status})`; - try { - const errorData = JSON.parse(errorText); - errorMsg = errorData.msg || errorData.message || errorData.error_description || errorMsg; - } catch (e) { - // Ignora erro de parse - } - throw new Error(errorMsg); - } - - const responseData = await response.json(); - const userId = responseData.user?.id || responseData.id; - - console.log('[DIRECT AUTH] Usuário criado:', userId); - - // NOTE: Role assignments MUST be done by the backend (Edge Function or server) - // when creating the user. The frontend should NOT attempt to assign roles. - // The backend should use the service role key to insert into user_roles table. - - return { - success: true, - user: { - id: userId, - email: input.email, - full_name: input.full_name, - phone: input.phone || null, - role: input.role, - }, - email: input.email, - password: input.password, - }; - - } catch (error: any) { - console.error('[DIRECT AUTH] Erro ao criar usuário:', error); - throw error; + } catch (networkErr) { + console.error('[USER API] Network error calling create-user', networkErr); + throw new Error('Erro de rede ao tentar criar usuário. Verifique sua conexão e tente novamente.'); + } + + // Parse com captura para logar resposta bruta e final + try { + const parsed = await parse(response); + try { console.debug('[USER API] create-user response:', parsed); } catch (_) {} + return parsed; + } catch (apiErr: any) { + // Já temos logs do parse em parse(), mas adicionamos um log contextual + console.error('[USER API] create-user failed:', apiErr?.message || apiErr); + throw apiErr; } } -// ============================================ -// CRIAÇÃO DE USUÁRIOS NO SUPABASE AUTH -// Vínculo com pacientes/médicos por EMAIL -// ============================================ - -// Criar usuário para MÉDICO no Supabase Auth (sistema de autenticação) -export async function criarUsuarioMedico(medico: { - email: string; - full_name: string; - phone_mobile: string; -}): Promise { - - const senha = gerarSenhaAleatoria(); - - console.log('[CRIAR MÉDICO] Iniciando criação no Supabase Auth...'); - console.log('Email:', medico.email); - console.log('Nome:', medico.full_name); - console.log('Telefone:', medico.phone_mobile); - console.log('Senha gerada:', senha); - - // Endpoint do Supabase Auth (mesmo que auth.ts usa) - const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`; - - const payload = { - email: medico.email, - password: senha, - data: { - userType: 'profissional', // Para login em /login -> /profissional - full_name: medico.full_name, - phone: medico.phone_mobile, - } - }; - - console.log('[CRIAR MÉDICO] Enviando para:', signupUrl); - - try { - const response = await fetch(signupUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "apikey": ENV_CONFIG.SUPABASE_ANON_KEY, - }, - body: JSON.stringify(payload), - }); - - console.log('[CRIAR MÉDICO] Status da resposta:', response.status, response.statusText); - - if (!response.ok) { - const errorText = await response.text(); - console.error('[CRIAR MÉDICO] Erro na resposta:', errorText); - - // Tenta parsear o erro para pegar mensagem específica - let errorMsg = `Erro ao criar usuário (${response.status})`; - try { - const errorData = JSON.parse(errorText); - errorMsg = errorData.msg || errorData.message || errorData.error_description || errorMsg; - - // Mensagens amigáveis para erros comuns - if (errorMsg.includes('already registered') || errorMsg.includes('already exists')) { - errorMsg = 'Este email já está cadastrado no sistema'; - } else if (errorMsg.includes('invalid email')) { - errorMsg = 'Formato de email inválido'; - } else if (errorMsg.includes('weak password')) { - errorMsg = 'Senha muito fraca'; - } - } catch (e) { - // Se não conseguir parsear, usa mensagem genérica - } - - throw new Error(errorMsg); - } - - const responseData = await response.json(); - console.log('[CRIAR MÉDICO] Usuário criado com sucesso no Supabase Auth!'); - console.log('User ID:', responseData.user?.id || responseData.id); - - // 🔧 AUTO-CONFIRMAR EMAIL: Fazer login automático logo após criar usuário - // Isso força o Supabase a confirmar o email automaticamente - if (responseData.user?.email_confirmed_at === null || !responseData.user?.email_confirmed_at) { - console.warn('[CRIAR MÉDICO] Email NÃO confirmado - tentando auto-confirmar via login...'); - - try { - const loginUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/token?grant_type=password`; - console.log('[AUTO-CONFIRMAR] Fazendo login automático para confirmar email...'); - - const loginResponse = await fetch(loginUrl, { - method: 'POST', - headers: { - 'apikey': ENV_CONFIG.SUPABASE_ANON_KEY, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: medico.email, - password: senha, - }), - }); - - if (loginResponse.ok) { - const loginData = await loginResponse.json(); - console.log('[AUTO-CONFIRMAR] Login automático realizado com sucesso!'); - console.log('[AUTO-CONFIRMAR] Email confirmado:', loginData.user?.email_confirmed_at ? 'SIM' : 'NÃO'); - - // Atualizar responseData com dados do login (que tem email confirmado) - if (loginData.user) { - responseData.user = loginData.user; - } - } else { - const errorText = await loginResponse.text(); - console.error('[AUTO-CONFIRMAR] Falha no login automático:', loginResponse.status, errorText); - console.warn('[AUTO-CONFIRMAR] Usuário pode não conseguir fazer login imediatamente!'); - } - } catch (confirmError) { - console.error('[AUTO-CONFIRMAR] Erro ao tentar fazer login automático:', confirmError); - console.warn('[AUTO-CONFIRMAR] Continuando sem confirmação automática...'); - } - } else { - console.log('[CRIAR MÉDICO] Email confirmado automaticamente!'); - } - - // Log bem visível com as credenciais para teste - console.log('========================================'); - console.log('CREDENCIAIS DO MÉDICO CRIADO:'); - console.log('Email:', medico.email); - console.log('Senha:', senha); - console.log('Pode fazer login?', responseData.user?.email_confirmed_at ? 'SIM' : 'NÃO (precisa confirmar email)'); - console.log('========================================'); - - return { - success: true, - user: responseData.user || responseData, - email: medico.email, - password: senha, - }; - - } catch (error: any) { - console.error('[CRIAR MÉDICO] Erro ao criar usuário:', error); - throw error; - } -} - -// Criar usuário para PACIENTE no Supabase Auth (sistema de autenticação) -export async function criarUsuarioPaciente(paciente: { - email: string; - full_name: string; - phone_mobile: string; -}): Promise { - - const senha = gerarSenhaAleatoria(); - - console.log('[CRIAR PACIENTE] Iniciando criação no Supabase Auth...'); - console.log('Email:', paciente.email); - console.log('Nome:', paciente.full_name); - console.log('Telefone:', paciente.phone_mobile); - console.log('Senha gerada:', senha); - - // Endpoint do Supabase Auth (mesmo que auth.ts usa) - const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`; - - const payload = { - email: paciente.email, - password: senha, - data: { - userType: 'paciente', // Para login em /login-paciente -> /paciente - full_name: paciente.full_name, - phone: paciente.phone_mobile, - } - }; - - console.log('[CRIAR PACIENTE] Enviando para:', signupUrl); - - try { - const response = await fetch(signupUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "apikey": ENV_CONFIG.SUPABASE_ANON_KEY, - }, - body: JSON.stringify(payload), - }); - - console.log('[CRIAR PACIENTE] Status da resposta:', response.status, response.statusText); - - if (!response.ok) { - const errorText = await response.text(); - console.error('[CRIAR PACIENTE] Erro na resposta:', errorText); - - // Tenta parsear o erro para pegar mensagem específica - let errorMsg = `Erro ao criar usuário (${response.status})`; - try { - const errorData = JSON.parse(errorText); - errorMsg = errorData.msg || errorData.message || errorData.error_description || errorMsg; - - // Mensagens amigáveis para erros comuns - if (errorMsg.includes('already registered') || errorMsg.includes('already exists')) { - errorMsg = 'Este email já está cadastrado no sistema'; - } else if (errorMsg.includes('invalid email')) { - errorMsg = 'Formato de email inválido'; - } else if (errorMsg.includes('weak password')) { - errorMsg = 'Senha muito fraca'; - } - } catch (e) { - // Se não conseguir parsear, usa mensagem genérica - } - - throw new Error(errorMsg); - } - - const responseData = await response.json(); - console.log('[CRIAR PACIENTE] Usuário criado com sucesso no Supabase Auth!'); - console.log('User ID:', responseData.user?.id || responseData.id); - console.log('[CRIAR PACIENTE] Resposta completa do Supabase:', JSON.stringify(responseData, null, 2)); - - // VERIFICAÇÃO CRÍTICA: O usuário foi realmente criado? - if (!responseData.user && !responseData.id) { - console.error('AVISO: Supabase retornou sucesso mas sem user ID!'); - console.error('Isso pode significar que o usuário não foi criado de verdade!'); - } - - const userId = responseData.user?.id || responseData.id; - - // 🔧 AUTO-CONFIRMAR EMAIL: Fazer login automático logo após criar usuário - // Isso força o Supabase a confirmar o email automaticamente - if (responseData.user?.email_confirmed_at === null || !responseData.user?.email_confirmed_at) { - console.warn('[CRIAR PACIENTE] Email NÃO confirmado - tentando auto-confirmar via login...'); - - try { - const loginUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/token?grant_type=password`; - console.log('[AUTO-CONFIRMAR] Fazendo login automático para confirmar email...'); - - const loginResponse = await fetch(loginUrl, { - method: 'POST', - headers: { - 'apikey': ENV_CONFIG.SUPABASE_ANON_KEY, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - email: paciente.email, - password: senha, - }), - }); - - console.log('[AUTO-CONFIRMAR] Status do login automático:', loginResponse.status); - - if (loginResponse.ok) { - const loginData = await loginResponse.json(); - console.log('[AUTO-CONFIRMAR] Login automático realizado com sucesso!'); - console.log('[AUTO-CONFIRMAR] Dados completos do login:', JSON.stringify(loginData, undefined, 2)); - console.log('[AUTO-CONFIRMAR] Email confirmado:', loginData.user?.email_confirmed_at ? 'SIM' : 'NÃO'); - console.log('[AUTO-CONFIRMAR] UserType no metadata:', loginData.user?.user_metadata?.userType); - console.log('[AUTO-CONFIRMAR] Email verified:', loginData.user?.user_metadata?.email_verified); - - // Atualizar responseData com dados do login (que tem email confirmado) - if (loginData.user) { - responseData.user = loginData.user; - } - } else { - const errorText = await loginResponse.text(); - console.error('[AUTO-CONFIRMAR] Falha no login automático:', loginResponse.status, errorText); - console.warn('[AUTO-CONFIRMAR] Usuário pode não conseguir fazer login imediatamente!'); - - // Tentar parsear o erro para entender melhor - try { - const errorData = JSON.parse(errorText); - console.error('[AUTO-CONFIRMAR] Detalhes do erro:', errorData); - } catch (e) { - console.error('[AUTO-CONFIRMAR] Erro não é JSON:', errorText); - } - } - } catch (confirmError) { - console.error('[AUTO-CONFIRMAR] Erro ao tentar fazer login automático:', confirmError); - console.warn('[AUTO-CONFIRMAR] Continuando sem confirmação automática...'); - } - } else { - console.log('[CRIAR PACIENTE] Email confirmado automaticamente!'); - } - - // Log bem visível com as credenciais para teste - console.log('========================================'); - console.log('CREDENCIAIS DO PACIENTE CRIADO:'); - console.log('Email:', paciente.email); - console.log('Senha:', senha); - console.log('UserType:', 'paciente'); - console.log('Pode fazer login?', responseData.user?.email_confirmed_at ? 'SIM' : 'NÃO (precisa confirmar email)'); - console.log('========================================'); - - return { - success: true, - user: responseData.user || responseData, - email: paciente.email, - password: senha, - }; - - } catch (error: any) { - console.error('[CRIAR PACIENTE] Erro ao criar usuário:', error); - throw error; - } -} // ===== CEP (usado nos formulários) ===== export async function buscarCepAPI(cep: string): Promise<{ diff --git a/susconecta/lib/auth.ts b/susconecta/lib/auth.ts index e21a67d..a66fe4c 100644 --- a/susconecta/lib/auth.ts +++ b/susconecta/lib/auth.ts @@ -83,245 +83,85 @@ async function processResponse(response: Response): Promise { /** * Serviço para fazer login e obter token JWT + * POST /auth/v1/token?grant_type=password */ export async function loginUser( email: string, - password: string, - userType: 'profissional' | 'paciente' | 'administrador' + password: string ): Promise { - let url = AUTH_ENDPOINTS.LOGIN; + const url = `${AUTH_ENDPOINTS.LOGIN}?grant_type=password`; + const headers = getLoginHeaders(); - const payload = { - email, - password, - }; - - console.log('[AUTH-API] Iniciando login...', { - email, - userType, - url, - payload, - timestamp: new Date().toLocaleTimeString() + const body = JSON.stringify({ email, password }); + + console.log('[AUTH] Login request:', { url, email }); + + const response = await fetch(url, { + method: 'POST', + headers, + body, }); - - 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)); - - try { - console.log('[AUTH-API] Enviando requisição de login...'); - - // Debug: Log request sem credenciais sensíveis - debugRequest('POST', url, getLoginHeaders(), payload); - - const 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 falhar, mostrar detalhes do erro - if (!response.ok) { - try { - const errorText = await response.text(); - console.error('[AUTH-API] Erro detalhado:', { - status: response.status, - statusText: response.statusText, - body: errorText, - headers: Object.fromEntries(response.headers.entries()) - }); - } catch (e) { - console.error('[AUTH-API] Não foi possível ler erro da resposta'); - } - } - - // Delay adicional para ver status code - await new Promise(resolve => setTimeout(resolve, 50)); - - const data = await processResponse(response); - - console.log('[AUTH] Dados recebidos da API:', data); - - // Verificar se recebemos os dados necessários - if (!data || (!data.access_token && !data.token)) { - console.error('[AUTH] API não retornou token válido:', data); - throw new AuthenticationError( - 'API não retornou token de acesso', - 'NO_TOKEN_RECEIVED', - data - ); - } - - // Adaptar resposta da sua API para o formato esperado - const adaptedResponse: LoginResponse = { - access_token: data.access_token || data.token, - token_type: data.token_type || "Bearer", - expires_in: data.expires_in || 3600, - user: { - id: data.user?.id || data.id || "1", - email: email, - name: data.user?.name || data.name || email.split('@')[0], - userType: userType, - profile: data.user?.profile || data.profile || {} - } - }; - - console.log('[AUTH-API] LOGIN REALIZADO COM SUCESSO!', { - token: adaptedResponse.access_token?.substring(0, 20) + '...', - user: { - email: adaptedResponse.user.email, - userType: adaptedResponse.user.userType - }, - timestamp: new Date().toLocaleTimeString() - }); - - // Delay final para visualizar sucesso - await new Promise(resolve => setTimeout(resolve, 50)); - - return adaptedResponse; - } catch (error) { - console.error('[AUTH] Erro no login:', error); - - if (error instanceof AuthenticationError) { - throw error; - } - - throw new AuthenticationError( - 'Email ou senha incorretos', - 'INVALID_CREDENTIALS', - error - ); - } + return processResponse(response); } /** * Serviço para fazer logout do usuário + * POST /auth/v1/logout */ export async function logoutUser(token: string): Promise { const url = AUTH_ENDPOINTS.LOGOUT; - - console.log('[AUTH-API] Fazendo logout na API...', { - url, - hasToken: !!token, - timestamp: new Date().toLocaleTimeString() + const headers = getAuthHeaders(token); + + console.log('[AUTH] Logout request'); + + const response = await fetch(url, { + method: 'POST', + headers, }); - // Delay para visualizar na aba Network - await new Promise(resolve => setTimeout(resolve, 400)); - - try { - console.log('[AUTH-API] Enviando requisição de logout...'); - - const response = await fetch(url, { - method: 'POST', - headers: getAuthHeaders(token), - }); - - console.log(`[AUTH-API] Logout response: ${response.status} ${response.statusText}`, { - timestamp: new Date().toLocaleTimeString() - }); - - // Delay para ver status code - await new Promise(resolve => setTimeout(resolve, 600)); - - // Logout pode retornar 200, 204 ou até 401 (se token já expirou) - // Todos são considerados "sucesso" para logout - if (response.ok || response.status === 401) { - console.log('[AUTH] Logout realizado com sucesso na API'); - return; - } - - // Se chegou aqui, algo deu errado mas não é crítico para logout - console.warn('[AUTH] API retornou status inesperado:', response.status); - - } catch (error) { - console.error('[AUTH] Erro ao chamar API de logout:', error); + if (!response.ok && response.status !== 204) { + await processResponse(response); } - - // Para logout, sempre continuamos mesmo com erro na API - // Isso evita que o usuário fique "preso" se a API estiver indisponível - console.log('[AUTH] Logout concluído (local sempre executado)'); } /** * Serviço para renovar token JWT + * POST /auth/v1/token?grant_type=refresh_token */ export async function refreshAuthToken(refreshToken: string): Promise { - const url = AUTH_ENDPOINTS.REFRESH; + const url = `${AUTH_ENDPOINTS.LOGIN}?grant_type=refresh_token`; + const headers = getLoginHeaders(); + + const body = JSON.stringify({ refresh_token: refreshToken }); + + console.log('[AUTH] Refresh token request'); + + const response = await fetch(url, { + method: 'POST', + headers, + body, + }); - console.log('[AUTH] Renovando token'); - - try { - const response = await fetch(url, { - method: 'POST', - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "apikey": ENV_CONFIG.SUPABASE_ANON_KEY, - }, - body: JSON.stringify({ refresh_token: refreshToken }), - }); - - const data = await processResponse(response); - - console.log('[AUTH] Token renovado com sucesso'); - return data; - } catch (error) { - console.error('[AUTH] Erro ao renovar token:', error); - - if (error instanceof AuthenticationError) { - throw error; - } - - throw new AuthenticationError( - 'Não foi possível renovar a sessão', - 'REFRESH_ERROR', - error - ); - } + return processResponse(response); } /** * Serviço para obter dados do usuário atual + * GET /auth/v1/user */ export async function getCurrentUser(token: string): Promise { const url = AUTH_ENDPOINTS.USER; + const headers = getAuthHeaders(token); + + console.log('[AUTH] Get current user request'); + + const response = await fetch(url, { + method: 'GET', + headers, + }); - console.log('[AUTH] Obtendo dados do usuário atual'); - - try { - const response = await fetch(url, { - method: 'GET', - headers: getAuthHeaders(token), - }); - - const data = await processResponse(response); - - console.log('[AUTH] Dados do usuário obtidos:', { id: data.id, email: data.email }); - return data; - } catch (error) { - console.error('[AUTH] Erro ao obter usuário atual:', error); - - if (error instanceof AuthenticationError) { - throw error; - } - - throw new AuthenticationError( - 'Não foi possível obter dados do usuário', - 'USER_DATA_ERROR', - error - ); - } + return processResponse(response); } /** diff --git a/susconecta/lib/config.ts b/susconecta/lib/config.ts index 6566c14..019ed05 100644 --- a/susconecta/lib/config.ts +++ b/susconecta/lib/config.ts @@ -6,7 +6,19 @@ export const API_CONFIG = { VERSION: "v1", } as const; -export const AUTH_ENDPOINTS = ENV_CONFIG.AUTH_ENDPOINTS; +export const AUTH_ENDPOINTS = { + LOGIN: `${ENV_CONFIG.SUPABASE_URL}/auth/v1/token`, + LOGOUT: `${ENV_CONFIG.SUPABASE_URL}/auth/v1/logout`, + USER: `${ENV_CONFIG.SUPABASE_URL}/auth/v1/user`, +} as const; + +export const FUNCTIONS_ENDPOINTS = { + USER_INFO: `${ENV_CONFIG.SUPABASE_URL}/functions/v1/user-info`, + // Use internal Next.js server route which performs privileged operations + // with the service role key. Client should call this route instead of + // calling the Supabase Edge Function directly. + CREATE_USER: `/api/create-user`, +} as const; export const API_KEY = ENV_CONFIG.SUPABASE_ANON_KEY; diff --git a/susconecta/lib/env-config.ts b/susconecta/lib/env-config.ts index cd41cda..8d416d9 100644 --- a/susconecta/lib/env-config.ts +++ b/susconecta/lib/env-config.ts @@ -64,13 +64,8 @@ export const ENV_CONFIG = { SUPABASE_ANON_KEY, PROJECT_REF: extractProjectRef(SUPABASE_URL), - // URLs dos endpoints de autenticação - AUTH_ENDPOINTS: { - LOGIN: `${SUPABASE_URL}/auth/v1/token?grant_type=password`, - LOGOUT: `${SUPABASE_URL}/auth/v1/logout`, - REFRESH: `${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, - USER: `${SUPABASE_URL}/auth/v1/user`, - }, + // Observação: endpoints de autenticação (admin) foram removidos do bundle cliente. + // Operações administrativas e privilégios devem ser executados no servidor. // Headers padrão DEFAULT_HEADERS: { diff --git a/susconecta/src/app/api/create-user/route.ts b/susconecta/src/app/api/create-user/route.ts new file mode 100644 index 0000000..a9f8a3e --- /dev/null +++ b/susconecta/src/app/api/create-user/route.ts @@ -0,0 +1,172 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { ENV_CONFIG } from '@/lib/env-config'; + +type CreateUserInput = { + email: string; + password?: string; + full_name: string; + phone?: string | null; + role?: string; + patient_id?: string | null; +}; + +function gerarSenhaAleatoria(): string { + const num1 = Math.floor(Math.random() * 10); + const num2 = Math.floor(Math.random() * 10); + const num3 = Math.floor(Math.random() * 10); + return `senha${num1}${num2}${num3}!`; +} + +function decodeJwtPayload(token: string): any | null { + try { + const parts = token.split('.'); + if (parts.length < 2) return null; + const payload = parts[1]; + // Node environment: use Buffer + const json = Buffer.from(payload.replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'); + return JSON.parse(json); + } catch (e) { + return null; + } +} + +async function requesterHasAdminRole(serviceKey: string, requesterId: string | undefined): Promise { + if (!requesterId) return false; + const url = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/user_roles?user_id=eq.${encodeURIComponent(String(requesterId))}`; + try { + const res = await fetch(url, { method: 'GET', headers: { apikey: serviceKey, Authorization: `Bearer ${serviceKey}`, Accept: 'application/json' } }); + if (!res.ok) return false; + const arr = await res.json().catch(() => []); + if (!Array.isArray(arr)) return false; + for (const r of arr) { + const role = (r?.role || '').toString(); + if (['admin', 'gestor', 'secretaria'].includes(role)) return true; + } + return false; + } catch (e) { + console.error('[create-user] failed to check requester roles', e); + return false; + } +} + +export async function POST(req: NextRequest) { + console.log('[create-user] POST handler invoked'); + const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY; + if (!serviceKey) { + return NextResponse.json({ error: 'server misconfigured: missing service role key' }, { status: 500 }); + } + + const authHeader = req.headers.get('authorization') || req.headers.get('Authorization'); + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return NextResponse.json({ error: 'Missing Authorization header' }, { status: 401 }); + } + + const clientToken = authHeader.replace(/^Bearer\s+/i, ''); + const payload = decodeJwtPayload(clientToken); + const requesterId = payload?.sub || payload?.user_id || payload?.id; + + const allowed = await requesterHasAdminRole(serviceKey, requesterId); + if (!allowed) return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 }); + + let body: CreateUserInput; + try { + body = await req.json(); + } catch (e) { + return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); + } + + if (!body?.email || !body?.full_name) { + return NextResponse.json({ error: 'Missing required fields: email, full_name' }, { status: 400 }); + } + + // call Edge Function create-user using service role key + const fnUrl = `${ENV_CONFIG.SUPABASE_URL}/functions/v1/create-user`; + try { + // ensure we have a password to show to the admin: generate one if caller didn't provide + const passwordToUse = body.password ?? gerarSenhaAleatoria(); + + const fnRes = await fetch(fnUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + apikey: serviceKey, + Authorization: `Bearer ${serviceKey}`, + }, + body: JSON.stringify({ + email: body.email, + password: passwordToUse, + full_name: body.full_name, + phone: body.phone ?? null, + role: body.role ?? 'user', + }), + }); + + const fnText = await fnRes.text(); + let fnJson: any = null; + try { fnJson = fnText ? JSON.parse(fnText) : null; } catch { fnJson = fnText; } + + if (!fnRes.ok) { + return NextResponse.json({ error: 'create-user function failed', details: fnJson ?? fnText }, { status: fnRes.status }); + } + + const createdUser = fnJson?.user ?? fnJson ?? null; + const userId = createdUser?.id ?? createdUser?.user_id ?? null; + + // include password in result when we generated/passed one so the client can display it + const result: any = { success: true, user: createdUser, password: passwordToUse ?? null, assignments: null, role_assignment: null }; + + const isProfessional = (body.role === 'medico' || body.role === 'enfermeiro'); + + // If request provided patient_id and user is professional, create assignment + if (isProfessional && body.patient_id && userId) { + try { + const assignUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/patient_assignments`; + const assignRes = await fetch(assignUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json', Accept: 'application/json', apikey: serviceKey, Authorization: `Bearer ${serviceKey}` }, + body: JSON.stringify({ patient_id: body.patient_id, user_id: userId, role: 'medico' }), + }); + const assignText = await assignRes.text(); + let assignJson: any = null; try { assignJson = assignText ? JSON.parse(assignText) : null; } catch { assignJson = assignText; } + result.assignments = assignRes.ok ? { ok: true, data: assignJson } : { ok: false, status: assignRes.status, details: assignJson ?? assignText }; + } catch (e) { + result.assignments = { ok: false, error: String(e) }; + } + } + + // NOTE: removed automatic assignment-by-email behavior. + // The server will no longer try to find a patient by email and create + // a patient_assignments row automatically when no `patient_id` is provided. + // This decision keeps assignment control in the UI: the admin should + // explicitly pick which patient (if any) to assign to the newly-created + // professional. If you want to re-enable automatic linking, restore this + // logic and ensure it's acceptable for your product policy. + if (isProfessional && !body.patient_id) { + console.log('[create-user] automatic patient-by-email assignment disabled; skip search'); + result.assign_by_email = { ok: false, reason: 'automatic_assignment_disabled' }; + } + + // insert into user_roles if requested + if (body.role && userId) { + try { + const rolesUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/user_roles`; + const roleRes = await fetch(rolesUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', apikey: serviceKey, Authorization: `Bearer ${serviceKey}` }, body: JSON.stringify({ user_id: userId, role: body.role }) }); + const roleText = await roleRes.text(); + let roleJson: any = null; try { roleJson = roleText ? JSON.parse(roleText) : null; } catch { roleJson = roleText; } + result.role_assignment = roleRes.ok ? { ok: true, data: roleJson } : { ok: false, status: roleRes.status, details: roleJson ?? roleText }; + } catch (e) { + result.role_assignment = { ok: false, error: String(e) }; + } + } + + return NextResponse.json(result); + } catch (e) { + console.error('[create-user] unexpected error', e); + return NextResponse.json({ error: 'internal error' }, { status: 500 }); + } +} + +export async function GET() { + return NextResponse.json({ ok: true, msg: 'create-user route alive' }); +} diff --git a/susconecta/src/app/api/link-paciente/route.ts b/susconecta/src/app/api/link-paciente/route.ts new file mode 100644 index 0000000..9cd4e9a --- /dev/null +++ b/susconecta/src/app/api/link-paciente/route.ts @@ -0,0 +1,80 @@ +import { NextResponse } from 'next/server' +import { ENV_CONFIG } from '@/lib/env-config' + +type Body = { + patientId: string + email: string +} + +export async function POST(req: Request) { + try { + const body = (await req.json()) as Body + if (!body || !body.patientId || !body.email) { + return NextResponse.json({ error: 'patientId and email are required' }, { status: 400 }) + } + + const svcKey = process.env.SUPABASE_SERVICE_ROLE_KEY + if (!svcKey) { + console.error('[link-paciente] SUPABASE_SERVICE_ROLE_KEY is missing') + return NextResponse.json({ error: 'server misconfigured' }, { status: 500 }) + } + + // 1) Buscar usuário no Auth por email usando endpoint admin + const usersUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/admin/users?email=${encodeURIComponent(body.email)}` + const uRes = await fetch(usersUrl, { + method: 'GET', + headers: { + Accept: 'application/json', + apikey: svcKey, + Authorization: `Bearer ${svcKey}`, + }, + }) + const uText = await uRes.text().catch(() => '') + let uJson: any = null + try { uJson = uText ? JSON.parse(uText) : null } catch { uJson = uText } + + if (!uRes.ok) { + console.error('[link-paciente] failed to fetch user by email', uRes.status, uJson ?? uText) + return NextResponse.json({ error: 'failed to lookup user', details: uJson ?? uText }, { status: uRes.status }) + } + + // The admin users endpoint can return an object or array depending on Supabase version; normalize + let foundUserId: string | null = null + if (Array.isArray(uJson) && uJson.length > 0) foundUserId = uJson[0].id + else if (uJson && uJson.id) foundUserId = uJson.id + + if (!foundUserId) { + return NextResponse.json({ ok: false, found: false, message: 'No auth user found for this email' }) + } + + // 2) Inserir role de aplicação (user/paciente) em user_roles + try { + const rolesUrl = `${ENV_CONFIG.SUPABASE_URL}/rest/v1/user_roles` + const roleRes = await fetch(rolesUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + apikey: svcKey, + Authorization: `Bearer ${svcKey}`, + }, + body: JSON.stringify({ user_id: foundUserId, role: 'user' }), + }) + const roleText = await roleRes.text().catch(() => '') + let roleJson: any = null + try { roleJson = roleText ? JSON.parse(roleText) : null } catch { roleJson = roleText } + if (!roleRes.ok) { + console.error('[link-paciente] failed to insert user_roles', roleRes.status, roleJson ?? roleText) + return NextResponse.json({ ok: false, assigned: false, details: roleJson ?? roleText }, { status: roleRes.status }) + } + + return NextResponse.json({ ok: true, userId: foundUserId, assigned: true, data: roleJson }) + } catch (e) { + console.error('[link-paciente] error inserting user_roles', e) + return NextResponse.json({ ok: false, error: String(e) }, { status: 500 }) + } + } catch (err) { + console.error('[link-paciente] unexpected error', err) + return NextResponse.json({ error: 'internal error' }, { status: 500 }) + } +}