Compare commits

...

12 Commits

Author SHA1 Message Date
107bba89d8 chore: save work 2025-10-08 20:56:35 -03:00
João Gustavo
0a7d3f3ae4 fix-laudos-module 2025-10-07 02:56:13 -03:00
João Gustavo
a43fdcc655 add-patient-page 2025-10-07 02:35:49 -03:00
João Gustavo
d7fbcab6a6 Merge branch 'develop' into feature/add-report-endpoints 2025-10-07 00:22:58 -03:00
João Gustavo
40bf3e0e6f fix-report-page 2025-10-07 00:16:39 -03:00
João Gustavo
a994a70d90 atualizing-api.ts 2025-10-06 23:24:57 -03:00
8955446bc7 Merge pull request 'feature/api-usuarios-perfis' (#40) from feature/api-usuarios-perfis into develop
Reviewed-on: #40
2025-10-07 01:51:25 +00:00
11c8b790ba Merge branch 'develop' into feature/api-usuarios-perfis 2025-10-06 22:47:22 -03:00
0510ef8a36 feat(api):add profile API 2025-10-06 02:35:21 -03:00
8284ccbadd Merge branch 'develop' into feature/api-usuarios-perfis 2025-10-04 23:44:02 -03:00
63e5a2ca9d chore(eslint): Configure and adjust
ESLint rules for the project
2025-10-03 14:38:54 -03:00
20f7d79474 feat: add email confirmation on user registration
Implements automatic creation in Supabase Auth with mandatory
email confirmation. Adds credentials popup and clear messages
about the confirmation process.

BREAKING CHANGE: Users must confirm email before login
2025-10-03 04:42:24 -03:00
53 changed files with 2311 additions and 288 deletions

View File

@ -0,0 +1,3 @@
{
"pages": {}
}

17
.next/build-manifest.json Normal file
View File

@ -0,0 +1,17 @@
{
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [
"static/development/_buildManifest.js",
"static/development/_ssgManifest.js"
],
"rootMainFiles": [],
"rootMainFilesTree": {},
"pages": {
"/_app": []
},
"ampFirstPages": []
}

1
.next/cache/.previewinfo vendored Normal file
View File

@ -0,0 +1 @@
{"previewModeId":"ef42988a66c7facb8cacc41264c816a3","previewModeSigningKey":"a79be007b8fc9844687acde60b0aaa42535a074e1b398db709f1ba36bb394a70","previewModeEncryptionKey":"581405036cabc30bb42611700f8f34afae8b4345617d5f849f74468b1d1b38d5","expireAt":1761141052491}

1
.next/cache/.rscinfo vendored Normal file
View File

@ -0,0 +1 @@
{"encryption.key":"1+xyFz3ckt+qN0qpo15cccDdyvzpv2iYyAOjplkpmUQ=","encryption.expire_at":1761141052419}

1
.next/cache/next-devtools-config.json vendored Normal file
View File

@ -0,0 +1 @@
{}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
.next/package.json Normal file
View File

@ -0,0 +1 @@
{"type": "commonjs"}

View File

@ -0,0 +1,11 @@
{
"version": 4,
"routes": {},
"dynamicRoutes": {},
"notFoundRoutes": [],
"preview": {
"previewModeId": "3a416f4d36dff8983ece0b1d649076fb",
"previewModeSigningKey": "5d2b597ca7226d41745e16f10542be9669d399be31d9ddb1d7d8740428afe5a2",
"previewModeEncryptionKey": "77da2f1b974f25583b0b427b5d5575be062291801610be0e6467ad97df7aaba3"
}
}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
{"version":3,"caseSensitive":false,"basePath":"","rewrites":{"beforeFiles":[],"afterFiles":[],"fallback":[]},"redirects":[{"source":"/:path+/","destination":"/:path+","permanent":true,"internal":true,"regex":"^(?:\\/((?:[^\\/]+?)(?:\\/(?:[^\\/]+?))*))\\/$"}],"headers":[]}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
self.__INTERCEPTION_ROUTE_REWRITE_MANIFEST="[]"

View File

@ -0,0 +1,19 @@
globalThis.__BUILD_MANIFEST = {
"polyfillFiles": [
"static/chunks/polyfills.js"
],
"devFiles": [],
"ampDevFiles": [],
"lowPriorityFiles": [],
"rootMainFiles": [],
"rootMainFilesTree": {},
"pages": {
"/_app": []
},
"ampFirstPages": []
};
globalThis.__BUILD_MANIFEST.lowPriorityFiles = [
"/static/" + process.env.__NEXT_BUILD_ID + "/_buildManifest.js",
,"/static/" + process.env.__NEXT_BUILD_ID + "/_ssgManifest.js",
];

View File

@ -0,0 +1,6 @@
{
"version": 3,
"middleware": {},
"functions": {},
"sortedMiddleware": []
}

View File

@ -0,0 +1 @@
self.__REACT_LOADABLE_MANIFEST="{}"

View File

@ -0,0 +1 @@
self.__NEXT_FONT_MANIFEST="{\"pages\":{},\"app\":{},\"appUsingSizeAdjust\":false,\"pagesUsingSizeAdjust\":false}"

View File

@ -0,0 +1 @@
{"pages":{},"app":{},"appUsingSizeAdjust":false,"pagesUsingSizeAdjust":false}

View File

@ -0,0 +1 @@
{}

View File

@ -0,0 +1 @@
self.__RSC_SERVER_MANIFEST="{\n \"node\": {},\n \"edge\": {},\n \"encryptionKey\": \"process.env.NEXT_SERVER_ACTIONS_ENCRYPTION_KEY\"\n}"

View File

@ -0,0 +1,5 @@
{
"node": {},
"edge": {},
"encryptionKey": "1+xyFz3ckt+qN0qpo15cccDdyvzpv2iYyAOjplkpmUQ="
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
self.__BUILD_MANIFEST = (function(a){return {__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},__routerFilterStatic:a,__routerFilterDynamic:a,sortedPages:["\u002F_app"]}}(void 0));self.__BUILD_MANIFEST_CB && self.__BUILD_MANIFEST_CB()

View File

@ -0,0 +1 @@
self.__SSG_MANIFEST=new Set;self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()

4
.next/trace Normal file

File diff suppressed because one or more lines are too long

141
.next/types/cache-life.d.ts vendored Normal file
View File

@ -0,0 +1,141 @@
// Type definitions for Next.js cacheLife configs
declare module 'next/cache' {
export { unstable_cache } from 'next/dist/server/web/spec-extension/unstable-cache'
export {
revalidateTag,
revalidatePath,
unstable_expireTag,
unstable_expirePath,
} from 'next/dist/server/web/spec-extension/revalidate'
export { unstable_noStore } from 'next/dist/server/web/spec-extension/unstable-no-store'
/**
* Cache this `"use cache"` for a timespan defined by the `"default"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 900 seconds (15 minutes)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 15 minutes, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function unstable_cacheLife(profile: "default"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"seconds"` profile.
* ```
* stale: 30 seconds
* revalidate: 1 seconds
* expire: 60 seconds (1 minute)
* ```
*
* This cache may be stale on clients for 30 seconds before checking with the server.
* If the server receives a new request after 1 seconds, start revalidating new values in the background.
* If this entry has no traffic for 1 minute it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "seconds"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"minutes"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 60 seconds (1 minute)
* expire: 3600 seconds (1 hour)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 minute, start revalidating new values in the background.
* If this entry has no traffic for 1 hour it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "minutes"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"hours"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 3600 seconds (1 hour)
* expire: 86400 seconds (1 day)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 hour, start revalidating new values in the background.
* If this entry has no traffic for 1 day it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "hours"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"days"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 86400 seconds (1 day)
* expire: 604800 seconds (1 week)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 day, start revalidating new values in the background.
* If this entry has no traffic for 1 week it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "days"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"weeks"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 604800 seconds (1 week)
* expire: 2592000 seconds (30 days)
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 1 week, start revalidating new values in the background.
* If this entry has no traffic for 30 days it will expire. The next request will recompute it.
*/
export function unstable_cacheLife(profile: "weeks"): void
/**
* Cache this `"use cache"` for a timespan defined by the `"max"` profile.
* ```
* stale: 300 seconds (5 minutes)
* revalidate: 2592000 seconds (30 days)
* expire: never
* ```
*
* This cache may be stale on clients for 5 minutes before checking with the server.
* If the server receives a new request after 30 days, start revalidating new values in the background.
* It lives for the maximum age of the server cache. If this entry has no traffic for a while, it may serve an old value the next request.
*/
export function unstable_cacheLife(profile: "max"): void
/**
* Cache this `"use cache"` using a custom timespan.
* ```
* stale: ... // seconds
* revalidate: ... // seconds
* expire: ... // seconds
* ```
*
* This is similar to Cache-Control: max-age=`stale`,s-max-age=`revalidate`,stale-while-revalidate=`expire-revalidate`
*
* If a value is left out, the lowest of other cacheLife() calls or the default, is used instead.
*/
export function unstable_cacheLife(profile: {
/**
* This cache may be stale on clients for ... seconds before checking with the server.
*/
stale?: number,
/**
* If the server receives a new request after ... seconds, start revalidating new values in the background.
*/
revalidate?: number,
/**
* If this entry has no traffic for ... seconds it will expire. The next request will recompute it.
*/
expire?: number
}): void
export { cacheTag as unstable_cacheTag } from 'next/dist/server/use-cache/cache-tag'
}

1
.next/types/package.json Normal file
View File

@ -0,0 +1 @@
{"type": "module"}

55
.next/types/routes.d.ts vendored Normal file
View File

@ -0,0 +1,55 @@
// This file is generated automatically by Next.js
// Do not edit this file manually
type AppRoutes = never
type PageRoutes = never
type LayoutRoutes = never
type RedirectRoutes = never
type RewriteRoutes = never
type Routes = AppRoutes | PageRoutes | LayoutRoutes | RedirectRoutes | RewriteRoutes
interface ParamMap {
}
export type ParamsOf<Route extends Routes> = ParamMap[Route]
interface LayoutSlotMap {
}
export type { AppRoutes, PageRoutes, LayoutRoutes, RedirectRoutes, RewriteRoutes, ParamMap }
declare global {
/**
* Props for Next.js App Router page components
* @example
* ```tsx
* export default function Page(props: PageProps<'/blog/[slug]'>) {
* const { slug } = await props.params
* return <div>Blog post: {slug}</div>
* }
* ```
*/
interface PageProps<AppRoute extends AppRoutes> {
params: Promise<ParamMap[AppRoute]>
searchParams: Promise<Record<string, string | string[] | undefined>>
}
/**
* Props for Next.js App Router layout components
* @example
* ```tsx
* export default function Layout(props: LayoutProps<'/dashboard'>) {
* return <div>{props.children}</div>
* }
* ```
*/
type LayoutProps<LayoutRoute extends LayoutRoutes> = {
params: Promise<ParamMap[LayoutRoute]>
children: React.ReactNode
} & {
[K in LayoutSlotMap[LayoutRoute]]: React.ReactNode
}
}

16
.next/types/validator.ts Normal file
View File

@ -0,0 +1,16 @@
// This file is generated automatically by Next.js
// Do not edit this file manually
// This file validates that all pages and layouts export the correct types

View File

@ -1,86 +1,263 @@
"use client";
import { Button } from "@/components/ui/button";
import { FileDown } from "lucide-react";
import { FileDown, BarChart2, Users, DollarSign, TrendingUp, UserCheck, CalendarCheck, ThumbsUp, User, Briefcase } from "lucide-react";
import jsPDF from "jspdf";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from "recharts";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, LineChart, Line, PieChart, Pie, Cell } from "recharts";
// Dados fictícios para demonstração
const metricas = [
{ label: "Atendimentos", value: 1240, icon: <CalendarCheck className="w-6 h-6 text-blue-500" /> },
{ label: "Absenteísmo", value: "7,2%", icon: <UserCheck className="w-6 h-6 text-red-500" /> },
{ label: "Satisfação", value: "92%", icon: <ThumbsUp className="w-6 h-6 text-green-500" /> },
{ label: "Faturamento (Mês)", value: "R$ 45.000", icon: <DollarSign className="w-6 h-6 text-emerald-500" /> },
{ label: "No-show", value: "5,1%", icon: <User className="w-6 h-6 text-yellow-500" /> },
];
const consultasPorPeriodo = [
{ periodo: "Jan", consultas: 210 },
{ periodo: "Fev", consultas: 180 },
{ periodo: "Mar", consultas: 250 },
{ periodo: "Abr", consultas: 230 },
{ periodo: "Mai", consultas: 270 },
{ periodo: "Jun", consultas: 220 },
];
const faturamentoMensal = [
{ mes: "Jan", valor: 35000 },
{ mes: "Fev", valor: 29000 },
{ mes: "Mar", valor: 42000 },
{ mes: "Abr", valor: 38000 },
{ mes: "Mai", valor: 45000 },
{ mes: "Jun", valor: 41000 },
];
const taxaNoShow = [
{ mes: "Jan", noShow: 6.2 },
{ mes: "Fev", noShow: 5.8 },
{ mes: "Mar", noShow: 4.9 },
{ mes: "Abr", noShow: 5.5 },
{ mes: "Mai", noShow: 5.1 },
{ mes: "Jun", noShow: 4.7 },
];
const pacientesMaisAtendidos = [
{ nome: "Ana Souza", consultas: 18 },
{ nome: "Bruno Lima", consultas: 15 },
{ nome: "Carla Menezes", consultas: 13 },
{ nome: "Diego Alves", consultas: 12 },
{ nome: "Fernanda Dias", consultas: 11 },
];
const medicosMaisProdutivos = [
{ nome: "Dr. Carlos Andrade", consultas: 62 },
{ nome: "Dra. Paula Silva", consultas: 58 },
{ nome: "Dr. João Pedro", consultas: 54 },
{ nome: "Dra. Marina Costa", consultas: 51 },
];
const convenios = [
{ nome: "Unimed", valor: 18000 },
{ nome: "Bradesco", valor: 12000 },
{ nome: "SulAmérica", valor: 9000 },
{ nome: "Particular", valor: 15000 },
];
const performancePorMedico = [
{ nome: "Dr. Carlos Andrade", consultas: 62, absenteismo: 4.8 },
{ nome: "Dra. Paula Silva", consultas: 58, absenteismo: 6.1 },
{ nome: "Dr. João Pedro", consultas: 54, absenteismo: 7.5 },
{ nome: "Dra. Marina Costa", consultas: 51, absenteismo: 5.2 },
];
const COLORS = ["#10b981", "#6366f1", "#f59e42", "#ef4444"];
function exportPDF(title: string, content: string) {
const doc = new jsPDF();
doc.text(title, 10, 10);
doc.text(content, 10, 20);
doc.save(`${title.toLowerCase().replace(/ /g, '-')}.pdf`);
}
export default function RelatoriosPage() {
// Dados fictícios para o gráfico financeiro
const financeiro = [
{ mes: "Jan", faturamento: 35000, despesas: 12000 },
{ mes: "Fev", faturamento: 29000, despesas: 15000 },
{ mes: "Mar", faturamento: 42000, despesas: 18000 },
{ mes: "Abr", faturamento: 38000, despesas: 14000 },
{ mes: "Mai", faturamento: 45000, despesas: 20000 },
{ mes: "Jun", faturamento: 41000, despesas: 17000 },
];
// ============================
// PASSO 3 - Funções de exportar
// ============================
const exportConsultasPDF = () => {
const doc = new jsPDF();
doc.text("Relatório de Consultas", 10, 10);
doc.text("Resumo das consultas realizadas.", 10, 20);
doc.save("relatorio-consultas.pdf");
};
const exportPacientesPDF = () => {
const doc = new jsPDF();
doc.text("Relatório de Pacientes", 10, 10);
doc.text("Informações gerais dos pacientes cadastrados.", 10, 20);
doc.save("relatorio-pacientes.pdf");
};
const exportFinanceiroPDF = () => {
const doc = new jsPDF();
doc.text("Relatório Financeiro", 10, 10);
doc.text("Receitas e despesas da clínica.", 10, 20);
doc.save("relatorio-financeiro.pdf");
};
return (
<div className="p-6 bg-background min-h-screen">
<h1 className="text-2xl font-bold mb-6 text-foreground">Relatórios</h1>
<h1 className="text-2xl font-bold mb-6 text-foreground">Dashboard Executivo de Relatórios</h1>
<div className="grid grid-cols-3 gap-6">
{/* Card Consultas */}
<div className="p-4 border border-border rounded-lg shadow bg-card">
<h2 className="font-semibold text-lg text-foreground">Relatório de Consultas</h2>
<p className="text-sm text-muted-foreground">Resumo das consultas realizadas.</p>
{/* PASSO 4 - Botão chama a função */}
<Button onClick={exportConsultasPDF} className="mt-4">
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF
</Button>
{/* Métricas principais */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 mb-8">
{metricas.map((m) => (
<div key={m.label} className="p-4 bg-card border border-border rounded-lg shadow flex flex-col items-center justify-center">
{m.icon}
<span className="text-2xl font-bold mt-2 text-foreground">{m.value}</span>
<span className="text-sm text-muted-foreground mt-1 text-center">{m.label}</span>
</div>
))}
</div>
{/* Gráficos e Relatórios */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Consultas realizadas por período */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><BarChart2 className="w-5 h-5" /> Consultas por Período</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={consultasPorPeriodo}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="periodo" />
<YAxis />
<Tooltip />
<Bar dataKey="consultas" fill="#6366f1" name="Consultas" />
</BarChart>
</ResponsiveContainer>
</div>
{/* Card Pacientes */}
<div className="p-4 border border-border rounded-lg shadow bg-card">
<h2 className="font-semibold text-lg text-foreground">Relatório de Pacientes</h2>
<p className="text-sm text-muted-foreground">Informações gerais dos pacientes cadastrados.</p>
<Button onClick={exportPacientesPDF} className="mt-4">
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF
</Button>
</div>
{/* Card Financeiro com gráfico */}
<div className="p-4 border border-border rounded-lg shadow col-span-3 md:col-span-3 bg-card">
<h2 className="font-semibold text-lg mb-2 text-foreground">Relatório Financeiro</h2>
<ResponsiveContainer width="100%" height={300}>
<BarChart data={financeiro} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
{/* Faturamento mensal/anual */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Faturamento Mensal</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Faturamento Mensal", "Resumo do faturamento mensal.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={faturamentoMensal}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mes" />
<YAxis />
<Tooltip />
<Legend />
<Bar dataKey="faturamento" fill="#10b981" name="Faturamento" />
<Bar dataKey="despesas" fill="#ef4444" name="Despesas" />
</BarChart>
<Line type="monotone" dataKey="valor" stroke="#10b981" name="Faturamento" strokeWidth={3} />
</LineChart>
</ResponsiveContainer>
<Button onClick={exportFinanceiroPDF} className="mt-4">
<FileDown className="mr-2 h-4 w-4" /> Exportar PDF
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Taxa de no-show */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><UserCheck className="w-5 h-5" /> Taxa de No-show</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Taxa de No-show", "Resumo da taxa de no-show.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<LineChart data={taxaNoShow}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="mes" />
<YAxis unit="%" />
<Tooltip />
<Line type="monotone" dataKey="noShow" stroke="#ef4444" name="No-show (%)" strokeWidth={3} />
</LineChart>
</ResponsiveContainer>
</div>
{/* Indicadores de satisfação */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><ThumbsUp className="w-5 h-5" /> Satisfação dos Pacientes</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Satisfação dos Pacientes", "Resumo dos indicadores de satisfação.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<div className="flex flex-col items-center justify-center h-[220px]">
<span className="text-5xl font-bold text-green-500">92%</span>
<span className="text-muted-foreground mt-2">Índice de satisfação geral</span>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Pacientes mais atendidos */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Users className="w-5 h-5" /> Pacientes Mais Atendidos</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
<tr className="text-muted-foreground">
<th className="text-left font-medium">Paciente</th>
<th className="text-left font-medium">Consultas</th>
</tr>
</thead>
<tbody>
{pacientesMaisAtendidos.map((p) => (
<tr key={p.nome}>
<td className="py-1">{p.nome}</td>
<td className="py-1">{p.consultas}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Médicos mais produtivos */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Briefcase className="w-5 h-5" /> Médicos Mais Produtivos</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
<tr className="text-muted-foreground">
<th className="text-left font-medium">Médico</th>
<th className="text-left font-medium">Consultas</th>
</tr>
</thead>
<tbody>
{medicosMaisProdutivos.map((m) => (
<tr key={m.nome}>
<td className="py-1">{m.nome}</td>
<td className="py-1">{m.consultas}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{/* Análise de convênios */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><DollarSign className="w-5 h-5" /> Análise de Convênios</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Análise de Convênios", "Resumo da análise de convênios.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<ResponsiveContainer width="100%" height={220}>
<PieChart>
<Pie data={convenios} dataKey="valor" nameKey="nome" cx="50%" cy="50%" outerRadius={80} label>
{convenios.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip />
<Legend />
</PieChart>
</ResponsiveContainer>
</div>
{/* Performance por médico */}
<div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><TrendingUp className="w-5 h-5" /> Performance por Médico</h2>
<Button size="sm" variant="outline" onClick={() => exportPDF("Performance por Médico", "Resumo da performance por médico.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div>
<table className="w-full text-sm mt-4">
<thead>
<tr className="text-muted-foreground">
<th className="text-left font-medium">Médico</th>
<th className="text-left font-medium">Consultas</th>
<th className="text-left font-medium">Absenteísmo (%)</th>
</tr>
</thead>
<tbody>
{performancePorMedico.map((m) => (
<tr key={m.nome}>
<td className="py-1">{m.nome}</td>
<td className="py-1">{m.consultas}</td>
<td className="py-1">{m.absenteismo}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>

View File

@ -33,7 +33,16 @@ export default function LoginPacientePage() {
console.error('[LOGIN-PACIENTE] Erro no login:', err)
if (err instanceof AuthenticationError) {
setError(err.message)
// Verificar se é erro de credenciais inválidas (pode ser email não confirmado)
if (err.code === '400' || err.details?.error_code === 'invalid_credentials') {
setError(
'⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' +
'verifique sua caixa de entrada e clique no link de confirmação ' +
'que foi enviado para ' + credentials.email
)
} else {
setError(err.message)
}
} else {
setError('Erro inesperado. Tente novamente.')
}

View File

@ -35,7 +35,16 @@ export default function LoginPage() {
console.error('[LOGIN-PROFISSIONAL] Erro no login:', err)
if (err instanceof AuthenticationError) {
setError(err.message)
// Verificar se é erro de credenciais inválidas (pode ser email não confirmado)
if (err.code === '400' || err.details?.error_code === 'invalid_credentials') {
setError(
'⚠️ Email ou senha incorretos. Se você acabou de se cadastrar, ' +
'verifique sua caixa de entrada e clique no link de confirmação ' +
'que foi enviado para ' + credentials.email
)
} else {
setError(err.message)
}
} else {
setError('Erro inesperado. Tente novamente.')
}

View File

@ -1,94 +1,577 @@
'use client'
import { useAuth } from '@/hooks/useAuth'
// import { useAuth } from '@/hooks/useAuth' // removido duplicado
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { User, LogOut, Home } from 'lucide-react'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { User, LogOut, Calendar, FileText, MessageCircle, UserCog, Home, Clock, FolderOpen, ChevronLeft, ChevronRight, MapPin, Stethoscope } from 'lucide-react'
import { SimpleThemeToggle } from '@/components/simple-theme-toggle'
import Link from 'next/link'
import ProtectedRoute from '@/components/ProtectedRoute'
import { useAuth } from '@/hooks/useAuth'
// Simulação de internacionalização básica
const strings = {
dashboard: 'Dashboard',
consultas: 'Consultas',
exames: 'Exames & Laudos',
mensagens: 'Mensagens',
perfil: 'Perfil',
sair: 'Sair',
proximaConsulta: 'Próxima Consulta',
ultimosExames: 'Últimos Exames',
mensagensNaoLidas: 'Mensagens Não Lidas',
agendar: 'Agendar',
reagendar: 'Reagendar',
cancelar: 'Cancelar',
detalhes: 'Detalhes',
adicionarCalendario: 'Adicionar ao calendário',
visualizarLaudo: 'Visualizar Laudo',
download: 'Download',
compartilhar: 'Compartilhar',
inbox: 'Caixa de Entrada',
enviarMensagem: 'Enviar Mensagem',
salvar: 'Salvar',
editarPerfil: 'Editar Perfil',
consentimentos: 'Consentimentos',
notificacoes: 'Preferências de Notificação',
vazio: 'Nenhum dado encontrado.',
erro: 'Ocorreu um erro. Tente novamente.',
carregando: 'Carregando...',
sucesso: 'Salvo com sucesso!',
erroSalvar: 'Erro ao salvar.',
}
export default function PacientePage() {
const { logout, user } = useAuth()
const [tab, setTab] = useState<'dashboard'|'consultas'|'exames'|'mensagens'|'perfil'>('dashboard')
// Simulação de loaders, empty states e erro
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [toast, setToast] = useState<{type: 'success'|'error', msg: string}|null>(null)
// Acessibilidade: foco visível e ordem de tabulação garantidos por padrão nos botões e inputs
const handleLogout = async () => {
console.log('[PACIENTE] Iniciando logout...')
await logout()
setLoading(true)
setError('')
try {
await logout()
} catch {
setError(strings.erro)
} finally {
setLoading(false)
}
}
// Estado para edição do perfil
const [isEditingProfile, setIsEditingProfile] = useState(false)
const [profileData, setProfileData] = useState({
nome: "Maria Silva Santos",
email: user?.email || "paciente@example.com",
telefone: "(11) 99999-9999",
endereco: "Rua das Flores, 123",
cidade: "São Paulo",
cep: "01234-567",
biografia: "Paciente desde 2020. Histórico de consultas e exames regulares.",
})
const handleProfileChange = (field: string, value: string) => {
setProfileData(prev => ({ ...prev, [field]: value }))
}
const handleSaveProfile = () => {
setIsEditingProfile(false)
setToast({ type: 'success', msg: strings.sucesso })
}
const handleCancelEdit = () => {
setIsEditingProfile(false)
}
function DashboardCards() {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<Card className="flex flex-col items-center justify-center p-4">
<Calendar className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.proximaConsulta}</span>
<span className="text-2xl">12/10/2025</span>
</Card>
<Card className="flex flex-col items-center justify-center p-4">
<FileText className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.ultimosExames}</span>
<span className="text-2xl">2</span>
</Card>
<Card className="flex flex-col items-center justify-center p-4">
<MessageCircle className="mb-2 text-primary" aria-hidden />
<span className="font-semibold">{strings.mensagensNaoLidas}</span>
<span className="text-2xl">1</span>
</Card>
</div>
)
}
// Consultas fictícias
const [currentDate, setCurrentDate] = useState(new Date())
const consultasFicticias = [
{
id: 1,
medico: "Dr. Carlos Andrade",
especialidade: "Cardiologia",
local: "Clínica Coração Feliz",
data: new Date().toISOString().split('T')[0],
hora: "09:00",
status: "Confirmada"
},
{
id: 2,
medico: "Dra. Fernanda Lima",
especialidade: "Dermatologia",
local: "Clínica Pele Viva",
data: new Date().toISOString().split('T')[0],
hora: "14:30",
status: "Pendente"
},
{
id: 3,
medico: "Dr. João Silva",
especialidade: "Ortopedia",
local: "Hospital Ortopédico",
data: (() => { let d = new Date(); d.setDate(d.getDate()+1); return d.toISOString().split('T')[0] })(),
hora: "11:00",
status: "Cancelada"
},
];
function formatDatePt(dateStr: string) {
const date = new Date(dateStr);
return date.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}
function navigateDate(direction: 'prev' | 'next') {
const newDate = new Date(currentDate);
newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1));
setCurrentDate(newDate);
}
function goToToday() {
setCurrentDate(new Date());
}
const todayStr = currentDate.toISOString().split('T')[0];
const consultasDoDia = consultasFicticias.filter(c => c.data === todayStr);
function Consultas() {
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold">Minhas Consultas</h2>
</div>
{/* Navegação de Data */}
<div className="flex items-center justify-between mb-6 p-4 bg-blue-50 rounded-lg dark:bg-muted">
<div className="flex items-center space-x-4">
<Button variant="outline" size="sm" onClick={() => navigateDate('prev')} className="p-2"><ChevronLeft className="h-4 w-4" /></Button>
<h3 className="text-lg font-medium text-foreground">{formatDatePt(todayStr)}</h3>
<Button variant="outline" size="sm" onClick={() => navigateDate('next')} className="p-2"><ChevronRight className="h-4 w-4" /></Button>
<Button variant="outline" size="sm" onClick={goToToday} className="ml-4 px-3 py-1 text-sm">Hoje</Button>
</div>
<div className="text-sm text-gray-600 dark:text-muted-foreground">
{consultasDoDia.length} consulta{consultasDoDia.length !== 1 ? 's' : ''} agendada{consultasDoDia.length !== 1 ? 's' : ''}
</div>
</div>
{/* Lista de Consultas do Dia */}
<div className="space-y-4">
{consultasDoDia.length === 0 ? (
<div className="text-center py-8 text-gray-600 dark:text-muted-foreground">
<Calendar className="h-12 w-12 mx-auto mb-4 text-gray-400 dark:text-muted-foreground/50" />
<p className="text-lg mb-2">Nenhuma consulta agendada para este dia</p>
<p className="text-sm">Você pode agendar uma nova consulta</p>
<Button variant="default" className="mt-4">Agendar Consulta</Button>
</div>
) : (
consultasDoDia.map(consulta => (
<div key={consulta.id} className="border-l-4 border-t border-r border-b p-4 rounded-lg shadow-sm bg-card border-border">
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 items-center">
<div className="flex items-center">
<div className="w-3 h-3 rounded-full mr-3" style={{ backgroundColor: consulta.status === 'Confirmada' ? '#22c55e' : consulta.status === 'Pendente' ? '#fbbf24' : '#ef4444' }}></div>
<div>
<div className="font-medium flex items-center">
<Stethoscope className="h-4 w-4 mr-2 text-gray-500 dark:text-muted-foreground" />
{consulta.medico}
</div>
<div className="text-sm text-gray-600 dark:text-muted-foreground">
{consulta.especialidade} {consulta.local}
</div>
</div>
</div>
<div className="flex items-center">
<Clock className="h-4 w-4 mr-2 text-gray-500 dark:text-muted-foreground" />
<span className="font-medium">{consulta.hora}</span>
</div>
<div className="flex items-center">
<div className={`px-3 py-1 rounded-full text-sm font-medium text-white ${consulta.status === 'Confirmada' ? 'bg-green-600' : consulta.status === 'Pendente' ? 'bg-yellow-500' : 'bg-red-600'}`}>{consulta.status}</div>
</div>
<div className="flex items-center justify-end space-x-2">
<Button variant="outline" size="sm">Detalhes</Button>
{consulta.status !== 'Cancelada' && <Button variant="secondary" size="sm">Reagendar</Button>}
{consulta.status !== 'Cancelada' && <Button variant="destructive" size="sm">Cancelar</Button>}
</div>
</div>
</div>
))
)}
</div>
</section>
)
}
// Exames e laudos fictícios
const examesFicticios = [
{
id: 1,
nome: "Hemograma Completo",
data: "2025-09-20",
status: "Disponível",
prontuario: "Paciente apresenta hemograma dentro dos padrões de normalidade. Sem alterações significativas.",
},
{
id: 2,
nome: "Raio-X de Tórax",
data: "2025-08-10",
status: "Disponível",
prontuario: "Exame radiológico sem evidências de lesões pulmonares. Estruturas cardíacas normais.",
},
{
id: 3,
nome: "Eletrocardiograma",
data: "2025-07-05",
status: "Disponível",
prontuario: "Ritmo sinusal, sem arritmias. Exame dentro da normalidade.",
},
];
const laudosFicticios = [
{
id: 1,
nome: "Laudo Hemograma Completo",
data: "2025-09-21",
status: "Assinado",
laudo: "Hemoglobina, hematócrito, leucócitos e plaquetas dentro dos valores de referência. Sem anemias ou infecções detectadas.",
},
{
id: 2,
nome: "Laudo Raio-X de Tórax",
data: "2025-08-11",
status: "Assinado",
laudo: "Radiografia sem alterações. Parênquima pulmonar preservado. Ausência de derrame pleural.",
},
{
id: 3,
nome: "Laudo Eletrocardiograma",
data: "2025-07-06",
status: "Assinado",
laudo: "ECG normal. Não há sinais de isquemia ou sobrecarga.",
},
];
const [exameSelecionado, setExameSelecionado] = useState<null | typeof examesFicticios[0]>(null)
const [laudoSelecionado, setLaudoSelecionado] = useState<null | typeof laudosFicticios[0]>(null)
function ExamesLaudos() {
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<h2 className="text-2xl font-bold mb-6">Exames</h2>
<div className="mb-8">
<h3 className="text-lg font-semibold mb-2">Meus Exames</h3>
<div className="space-y-3">
{examesFicticios.map(exame => (
<div key={exame.id} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
<div>
<div className="font-medium text-foreground">{exame.nome}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(exame.data).toLocaleDateString('pt-BR')}</div>
</div>
<div className="flex gap-2 mt-2 md:mt-0">
<Button variant="outline" onClick={() => setExameSelecionado(exame)}>Ver Prontuário</Button>
<Button variant="secondary">Download</Button>
</div>
</div>
))}
</div>
</div>
<h2 className="text-2xl font-bold mb-6">Laudos</h2>
<div>
<h3 className="text-lg font-semibold mb-2">Meus Laudos</h3>
<div className="space-y-3">
{laudosFicticios.map(laudo => (
<div key={laudo.id} className="flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4">
<div>
<div className="font-medium text-foreground">{laudo.nome}</div>
<div className="text-sm text-muted-foreground">Data: {new Date(laudo.data).toLocaleDateString('pt-BR')}</div>
</div>
<div className="flex gap-2 mt-2 md:mt-0">
<Button variant="outline" onClick={() => setLaudoSelecionado(laudo)}>Visualizar</Button>
<Button variant="secondary">Compartilhar</Button>
</div>
</div>
))}
</div>
</div>
{/* Modal Prontuário Exame */}
<Dialog open={!!exameSelecionado} onOpenChange={open => !open && setExameSelecionado(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Prontuário do Exame</DialogTitle>
<DialogDescription>
{exameSelecionado && (
<>
<div className="font-semibold mb-2">{exameSelecionado.nome}</div>
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(exameSelecionado.data).toLocaleDateString('pt-BR')}</div>
<div className="mb-4 whitespace-pre-line">{exameSelecionado.prontuario}</div>
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setExameSelecionado(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Modal Visualizar Laudo */}
<Dialog open={!!laudoSelecionado} onOpenChange={open => !open && setLaudoSelecionado(null)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Laudo Médico</DialogTitle>
<DialogDescription>
{laudoSelecionado && (
<>
<div className="font-semibold mb-2">{laudoSelecionado.nome}</div>
<div className="text-sm text-muted-foreground mb-4">Data: {new Date(laudoSelecionado.data).toLocaleDateString('pt-BR')}</div>
<div className="mb-4 whitespace-pre-line">{laudoSelecionado.laudo}</div>
</>
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => setLaudoSelecionado(null)}>Fechar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</section>
)
}
// Mensagens fictícias recebidas do médico
const mensagensFicticias = [
{
id: 1,
medico: "Dr. Carlos Andrade",
data: "2025-10-06T15:30:00",
conteudo: "Olá Maria, seu exame de hemograma está normal. Parabéns por manter seus exames em dia!",
lida: false
},
{
id: 2,
medico: "Dra. Fernanda Lima",
data: "2025-09-21T10:15:00",
conteudo: "Maria, seu laudo de Raio-X já está disponível no sistema. Qualquer dúvida, estou à disposição.",
lida: true
},
{
id: 3,
medico: "Dr. João Silva",
data: "2025-08-12T09:00:00",
conteudo: "Bom dia! Lembre-se de agendar seu retorno para acompanhamento da ortopedia.",
lida: true
},
];
function Mensagens() {
return (
<section className="bg-card shadow-md rounded-lg border border-border p-6">
<h2 className="text-2xl font-bold mb-6">Mensagens Recebidas</h2>
<div className="space-y-3">
{mensagensFicticias.length === 0 ? (
<div className="text-center py-8 text-gray-600 dark:text-muted-foreground">
<MessageCircle className="h-12 w-12 mx-auto mb-4 text-gray-400 dark:text-muted-foreground/50" />
<p className="text-lg mb-2">Nenhuma mensagem recebida</p>
<p className="text-sm">Você ainda não recebeu mensagens dos seus médicos.</p>
</div>
) : (
mensagensFicticias.map(msg => (
<div key={msg.id} className={`flex flex-col md:flex-row md:items-center md:justify-between bg-muted rounded p-4 border ${!msg.lida ? 'border-primary' : 'border-transparent'}`}>
<div>
<div className="font-medium text-foreground flex items-center gap-2">
<User className="h-4 w-4 text-primary" />
{msg.medico}
{!msg.lida && <span className="ml-2 px-2 py-0.5 rounded-full text-xs bg-primary text-white">Nova</span>}
</div>
<div className="text-sm text-muted-foreground mb-2">{new Date(msg.data).toLocaleString('pt-BR')}</div>
<div className="text-foreground whitespace-pre-line">{msg.conteudo}</div>
</div>
</div>
))
)}
</div>
</section>
)
}
function Perfil() {
return (
<div className="space-y-6 max-w-2xl mx-auto">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold text-foreground">Meu Perfil</h2>
{!isEditingProfile ? (
<Button onClick={() => setIsEditingProfile(true)} className="flex items-center gap-2">
Editar Perfil
</Button>
) : (
<div className="flex gap-2">
<Button onClick={handleSaveProfile} className="flex items-center gap-2">Salvar</Button>
<Button variant="outline" onClick={handleCancelEdit}>Cancelar</Button>
</div>
)}
</div>
<div className="grid gap-6 md:grid-cols-2">
{/* Informações Pessoais */}
<div className="space-y-4">
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">Informações Pessoais</h3>
<div className="space-y-2">
<Label htmlFor="nome">Nome Completo</Label>
<p className="p-2 bg-muted rounded text-muted-foreground">{profileData.nome}</p>
<span className="text-xs text-muted-foreground">Este campo não pode ser alterado</span>
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
{isEditingProfile ? (
<Input id="email" type="email" value={profileData.email} onChange={e => handleProfileChange('email', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.email}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label>
{isEditingProfile ? (
<Input id="telefone" value={profileData.telefone} onChange={e => handleProfileChange('telefone', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.telefone}</p>
)}
</div>
</div>
{/* Endereço e Contato */}
<div className="space-y-4">
<h3 className="text-lg font-semibold border-b border-border text-foreground pb-2">Endereço</h3>
<div className="space-y-2">
<Label htmlFor="endereco">Endereço</Label>
{isEditingProfile ? (
<Input id="endereco" value={profileData.endereco} onChange={e => handleProfileChange('endereco', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.endereco}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="cidade">Cidade</Label>
{isEditingProfile ? (
<Input id="cidade" value={profileData.cidade} onChange={e => handleProfileChange('cidade', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cidade}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="cep">CEP</Label>
{isEditingProfile ? (
<Input id="cep" value={profileData.cep} onChange={e => handleProfileChange('cep', e.target.value)} />
) : (
<p className="p-2 bg-muted/50 rounded text-foreground">{profileData.cep}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="biografia">Biografia</Label>
{isEditingProfile ? (
<Textarea id="biografia" value={profileData.biografia} onChange={e => handleProfileChange('biografia', e.target.value)} rows={4} placeholder="Conte um pouco sobre você..." />
) : (
<p className="p-2 bg-muted/50 rounded min-h-[100px] text-foreground">{profileData.biografia}</p>
)}
</div>
</div>
</div>
{/* Foto do Perfil */}
<div className="border-t border-border pt-6">
<h3 className="text-lg font-semibold mb-4 text-foreground">Foto do Perfil</h3>
<div className="flex items-center gap-4">
<Avatar className="h-20 w-20">
<AvatarFallback className="text-lg">
{profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()}
</AvatarFallback>
</Avatar>
{isEditingProfile && (
<div className="space-y-2">
<Button variant="outline" size="sm">Alterar Foto</Button>
<p className="text-xs text-muted-foreground">Formatos aceitos: JPG, PNG (máx. 2MB)</p>
</div>
)}
</div>
</div>
</div>
)
}
// Renderização principal
return (
<ProtectedRoute requiredUserType={["paciente"]}>
<div className="min-h-screen bg-gray-50 flex items-center justify-center p-4">
<Card className="w-full max-w-md shadow-lg">
<CardHeader className="text-center">
<div className="mx-auto w-16 h-16 bg-primary/10 rounded-full flex items-center justify-center mb-4">
<User className="h-8 w-8 text-primary" />
</div>
<CardTitle className="text-2xl font-bold text-gray-900">
Portal do Paciente
</CardTitle>
<p className="text-sm text-gray-600">
Bem-vindo ao seu espaço pessoal
</p>
</CardHeader>
<CardContent className="space-y-6">
{/* Informações do Paciente */}
<div className="text-center">
<h2 className="text-xl font-semibold text-gray-800 mb-2">
Maria Silva Santos
</h2>
<p className="text-sm text-gray-600">
CPF: 123.456.789-00
</p>
<p className="text-sm text-gray-600">
Idade: 35 anos
</p>
</div>
{/* Informações do Login */}
<div className="bg-gray-100 rounded-lg p-4">
<div className="text-center">
<p className="text-sm text-gray-600 mb-1">
Conectado como:
</p>
<p className="font-medium text-gray-800">
{user?.email || 'paciente@example.com'}
</p>
<p className="text-xs text-gray-500 mt-1">
Tipo de usuário: Paciente
</p>
</div>
</div>
{/* Botão Voltar ao Início */}
<Button
asChild
variant="outline"
className="w-full flex items-center justify-center gap-2 cursor-pointer"
>
<div className="min-h-screen bg-background text-foreground flex flex-col">
{/* Header só com título e botão de sair */}
<header className="flex items-center justify-between px-4 py-2 border-b bg-card">
<div className="flex items-center gap-2">
<User className="h-6 w-6 text-primary" aria-hidden />
<span className="font-bold">Portal do Paciente</span>
<Button asChild variant="outline" className="ml-4">
<Link href="/">
<Home className="h-4 w-4" />
Voltar ao Início
<Home className="h-4 w-4 mr-1" /> Início
</Link>
</Button>
</div>
<div className="flex items-center gap-2">
<SimpleThemeToggle />
<Button onClick={handleLogout} variant="destructive" aria-label={strings.sair} disabled={loading} className="ml-2"><LogOut className="h-4 w-4" /> {strings.sair}</Button>
</div>
</header>
{/* Botão de Logout */}
<Button
onClick={handleLogout}
variant="destructive"
className="w-full flex items-center justify-center gap-2 cursor-pointer"
>
<LogOut className="h-4 w-4" />
Sair
</Button>
<div className="flex flex-1 min-h-0">
{/* Sidebar vertical */}
<nav aria-label="Navegação do dashboard" className="w-56 bg-card border-r flex flex-col py-6 px-2 gap-2">
<Button variant={tab==='dashboard'?'secondary':'ghost'} aria-current={tab==='dashboard'} onClick={()=>setTab('dashboard')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.dashboard}</Button>
<Button variant={tab==='consultas'?'secondary':'ghost'} aria-current={tab==='consultas'} onClick={()=>setTab('consultas')} className="justify-start"><Calendar className="mr-2 h-5 w-5" />{strings.consultas}</Button>
<Button variant={tab==='exames'?'secondary':'ghost'} aria-current={tab==='exames'} onClick={()=>setTab('exames')} className="justify-start"><FileText className="mr-2 h-5 w-5" />{strings.exames}</Button>
<Button variant={tab==='mensagens'?'secondary':'ghost'} aria-current={tab==='mensagens'} onClick={()=>setTab('mensagens')} className="justify-start"><MessageCircle className="mr-2 h-5 w-5" />{strings.mensagens}</Button>
<Button variant={tab==='perfil'?'secondary':'ghost'} aria-current={tab==='perfil'} onClick={()=>setTab('perfil')} className="justify-start"><UserCog className="mr-2 h-5 w-5" />{strings.perfil}</Button>
</nav>
{/* Conteúdo principal */}
<div className="flex-1 min-w-0 p-4 max-w-4xl mx-auto w-full">
{/* Toasts de feedback */}
{toast && (
<div className={`fixed top-4 right-4 z-50 px-4 py-2 rounded shadow-lg ${toast.type==='success'?'bg-green-600 text-white':'bg-red-600 text-white'}`} role="alert">{toast.msg}</div>
)}
{/* Informação adicional */}
<div className="text-center">
<p className="text-xs text-gray-500">
Em breve, mais funcionalidades estarão disponíveis
</p>
</div>
</CardContent>
</Card>
{/* Loader global */}
{loading && <div className="flex-1 flex items-center justify-center"><span>{strings.carregando}</span></div>}
{error && <div className="flex-1 flex items-center justify-center text-red-600"><span>{error}</span></div>}
{/* Conteúdo principal */}
{!loading && !error && (
<main className="flex-1">
{tab==='dashboard' && <DashboardCards />}
{tab==='consultas' && <Consultas />}
{tab==='exames' && <ExamesLaudos />}
{tab==='mensagens' && <Mensagens />}
{tab==='perfil' && <Perfil />}
</main>
)}
</div>
</div>
</div>
</ProtectedRoute>
)

View File

@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
import {
Table,
TableBody,
@ -2032,14 +2033,40 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
}
}, [laudo, isNewLaudo]);
const formatText = (type: string) => {
// Histórico para desfazer/refazer
const [history, setHistory] = useState<string[]>([]);
const [historyIndex, setHistoryIndex] = useState(-1);
// Atualiza histórico ao digitar
useEffect(() => {
if (history[historyIndex] !== content) {
const newHistory = history.slice(0, historyIndex + 1);
setHistory([...newHistory, content]);
setHistoryIndex(newHistory.length);
}
// eslint-disable-next-line
}, [content]);
const handleUndo = () => {
if (historyIndex > 0) {
setContent(history[historyIndex - 1]);
setHistoryIndex(historyIndex - 1);
}
};
const handleRedo = () => {
if (historyIndex < history.length - 1) {
setContent(history[historyIndex + 1]);
setHistoryIndex(historyIndex + 1);
}
};
// Formatação avançada
const formatText = (type: string, value?: any) => {
const textarea = document.querySelector('textarea') as HTMLTextAreaElement;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
let formattedText = "";
switch(type) {
case "bold":
@ -2049,13 +2076,44 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
formattedText = selectedText ? `*${selectedText}*` : "*texto em itálico*";
break;
case "underline":
formattedText = selectedText ? `<u>${selectedText}</u>` : "<u>texto sublinhado</u>";
formattedText = selectedText ? `__${selectedText}__` : "__texto sublinhado__";
break;
case "list":
formattedText = selectedText ? `${selectedText}` : "• item da lista";
case "list-ul":
formattedText = selectedText ? selectedText.split('\n').map(l => `${l}`).join('\n') : "• item da lista";
break;
case "list-ol":
formattedText = selectedText ? selectedText.split('\n').map((l,i) => `${i+1}. ${l}`).join('\n') : "1. item da lista";
break;
case "indent":
formattedText = selectedText ? selectedText.split('\n').map(l => ` ${l}`).join('\n') : " ";
break;
case "outdent":
formattedText = selectedText ? selectedText.split('\n').map(l => l.replace(/^\s{1,4}/, "")).join('\n') : "";
break;
case "align-left":
formattedText = selectedText ? `[left]${selectedText}[/left]` : "[left]Texto à esquerda[/left]";
break;
case "align-center":
formattedText = selectedText ? `[center]${selectedText}[/center]` : "[center]Texto centralizado[/center]";
break;
case "align-right":
formattedText = selectedText ? `[right]${selectedText}[/right]` : "[right]Texto à direita[/right]";
break;
case "align-justify":
formattedText = selectedText ? `[justify]${selectedText}[/justify]` : "[justify]Texto justificado[/justify]";
break;
case "font-size":
formattedText = selectedText ? `[size=${value}]${selectedText}[/size]` : `[size=${value}]Texto tamanho ${value}[/size]`;
break;
case "font-family":
formattedText = selectedText ? `[font=${value}]${selectedText}[/font]` : `[font=${value}]${value}[/font]`;
break;
case "font-color":
formattedText = selectedText ? `[color=${value}]${selectedText}[/color]` : `[color=${value}]${value}[/color]`;
break;
default:
return;
}
const newText = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end);
setContent(newText);
};
@ -2084,7 +2142,14 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
return content
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.*?)\*/g, '<em>$1</em>')
.replace(/<u>(.*?)<\/u>/g, '<u>$1</u>')
.replace(/__(.*?)__/g, '<u>$1</u>')
.replace(/\[left\](.*?)\[\/left\]/gs, '<div style="text-align:left">$1</div>')
.replace(/\[center\](.*?)\[\/center\]/gs, '<div style="text-align:center">$1</div>')
.replace(/\[right\](.*?)\[\/right\]/gs, '<div style="text-align:right">$1</div>')
.replace(/\[justify\](.*?)\[\/justify\]/gs, '<div style="text-align:justify">$1</div>')
.replace(/\[size=(\d+)\](.*?)\[\/size\]/gs, '<span style="font-size:$1px">$2</span>')
.replace(/\[font=([^\]]+)\](.*?)\[\/font\]/gs, '<span style="font-family:$1">$2</span>')
.replace(/\[color=([^\]]+)\](.*?)\[\/color\]/gs, '<span style="color:$1">$2</span>')
.replace(/{{sexo_paciente}}/g, pacienteSelecionado?.sexo || laudo?.paciente?.sexo || '[SEXO]')
.replace(/{{diagnostico}}/g, campos.diagnostico || '[DIAGNÓSTICO]')
.replace(/{{conclusao}}/g, campos.conclusao || '[CONCLUSÃO]')
@ -2383,44 +2448,60 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
<div className="flex-1 flex flex-col">
{/* Toolbar */}
<div className="p-3 border-b border-border">
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
onClick={() => formatText("bold")}
title="Negrito"
className="hover:bg-blue-50 dark:hover:bg-accent"
<div className="flex flex-wrap gap-2 items-center">
{/* Tamanho da fonte */}
<label className="text-xs mr-1">Tamanho</label>
<input
type="number"
min={8}
max={32}
defaultValue={14}
onBlur={e => formatText('font-size', e.target.value)}
className="w-14 border rounded px-1 py-0.5 text-xs mr-2"
title="Tamanho da fonte"
/>
{/* Família da fonte */}
<label className="text-xs mr-1">Fonte</label>
<select
defaultValue={'Arial'}
onBlur={e => formatText('font-family', e.target.value)}
className="border rounded px-1 py-0.5 text-xs mr-2"
title="Família da fonte"
>
<strong>B</strong>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => formatText("italic")}
title="Itálico"
className="hover:bg-blue-50 dark:hover:bg-accent"
>
<em>I</em>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => formatText("underline")}
title="Sublinhado"
className="hover:bg-blue-50 dark:hover:bg-accent"
>
<u>U</u>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => formatText("list")}
title="Lista"
className="hover:bg-blue-50 dark:hover:bg-accent"
>
</Button>
<option value="Arial">Arial</option>
<option value="Helvetica">Helvetica</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Courier New">Courier New</option>
<option value="Verdana">Verdana</option>
<option value="Georgia">Georgia</option>
</select>
{/* Cor da fonte */}
<label className="text-xs mr-1">Cor</label>
<input
type="color"
defaultValue="#222222"
onBlur={e => formatText('font-color', e.target.value)}
className="w-6 h-6 border rounded mr-2"
title="Cor da fonte"
/>
{/* Alinhamento */}
<Button variant="outline" size="sm" onClick={() => formatText('align-left')} title="Alinhar à esquerda" className="px-1"><svg width="16" height="16" fill="none"><rect x="2" y="4" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
<Button variant="outline" size="sm" onClick={() => formatText('align-center')} title="Centralizar" className="px-1"><svg width="16" height="16" fill="none"><rect x="4" y="4" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="3" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
<Button variant="outline" size="sm" onClick={() => formatText('align-right')} title="Alinhar à direita" className="px-1"><svg width="16" height="16" fill="none"><rect x="6" y="4" width="8" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="4" y="10" width="10" height="2" rx="1" fill="currentColor"/></svg></Button>
<Button variant="outline" size="sm" onClick={() => formatText('align-justify')} title="Justificar" className="px-1"><svg width="16" height="16" fill="none"><rect x="2" y="4" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="7" width="12" height="2" rx="1" fill="currentColor"/><rect x="2" y="10" width="12" height="2" rx="1" fill="currentColor"/></svg></Button>
{/* Listas */}
<Button variant="outline" size="sm" onClick={() => formatText('list-ol')} title="Lista numerada" className="px-1">1.</Button>
<Button variant="outline" size="sm" onClick={() => formatText('list-ul')} title="Lista com marcadores" className="px-1"></Button>
{/* Recuo */}
<Button variant="outline" size="sm" onClick={() => formatText('indent')} title="Aumentar recuo" className="px-1"></Button>
<Button variant="outline" size="sm" onClick={() => formatText('outdent')} title="Diminuir recuo" className="px-1"></Button>
{/* Desfazer/Refazer */}
<Button variant="outline" size="sm" onClick={handleUndo} title="Desfazer" className="px-1"></Button>
<Button variant="outline" size="sm" onClick={handleRedo} title="Refazer" className="px-1"></Button>
{/* Negrito, itálico, sublinhado */}
<Button variant="outline" size="sm" onClick={() => formatText("bold") } title="Negrito" className="hover:bg-blue-50 dark:hover:bg-accent"><strong>B</strong></Button>
<Button variant="outline" size="sm" onClick={() => formatText("italic") } title="Itálico" className="hover:bg-blue-50 dark:hover:bg-accent"><em>I</em></Button>
<Button variant="outline" size="sm" onClick={() => formatText("underline") } title="Sublinhado" className="hover:bg-blue-50 dark:hover:bg-accent"><u>U</u></Button>
</div>
{/* Templates */}
@ -2443,12 +2524,13 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
</div>
{/* Editor */}
<div className="flex-1 p-4">
<div className="flex-1 p-4 overflow-auto max-h-[500px]">
<Textarea
value={content}
onChange={(e) => setContent(e.target.value)}
placeholder="Digite o conteúdo do laudo aqui. Use ** para negrito, * para itálico, <u></u> para sublinhado."
className="h-full min-h-[400px] resize-none"
className="h-full min-h-[400px] resize-none scrollbar-thin scrollbar-thumb-blue-400 scrollbar-track-blue-100"
style={{ maxHeight: 400, overflow: 'auto' }}
/>
</div>
</div>
@ -3270,13 +3352,16 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
)}
</div>
</div>
<Button
variant="outline"
onClick={logout}
className="text-red-600 border-red-600 hover:bg-red-50 cursor-pointer dark:hover:bg-red-600 dark:hover:text-white"
>
Sair
</Button>
<div className="flex items-center gap-2">
<SimpleThemeToggle />
<Button
variant="outline"
onClick={logout}
className="text-red-600 border-red-600 hover:bg-red-50 cursor-pointer dark:hover:bg-red-600 dark:hover:text-white"
>
Sair
</Button>
</div>
</header>
<div className="grid grid-cols-1 md:grid-cols-[220px_1fr] gap-6">

View File

@ -0,0 +1,177 @@
"use client";
import { useState } from "react";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { CheckCircle2, Copy, Eye, EyeOff } from "lucide-react";
import { Alert, AlertDescription } from "@/components/ui/alert";
export interface CredentialsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
email: string;
password: string;
userName: string;
userType: "médico" | "paciente";
}
export function CredentialsDialog({
open,
onOpenChange,
email,
password,
userName,
userType,
}: CredentialsDialogProps) {
const [showPassword, setShowPassword] = useState(false);
const [copiedEmail, setCopiedEmail] = useState(false);
const [copiedPassword, setCopiedPassword] = useState(false);
function handleCopyEmail() {
navigator.clipboard.writeText(email);
setCopiedEmail(true);
setTimeout(() => setCopiedEmail(false), 2000);
}
function handleCopyPassword() {
navigator.clipboard.writeText(password);
setCopiedPassword(true);
setTimeout(() => setCopiedPassword(false), 2000);
}
function handleCopyBoth() {
const text = `Email: ${email}\nSenha: ${password}`;
navigator.clipboard.writeText(text);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-500" />
{userType === "médico" ? "Médico" : "Paciente"} Cadastrado com Sucesso!
</DialogTitle>
<DialogDescription>
O {userType} <strong>{userName}</strong> foi cadastrado e pode fazer login com as credenciais abaixo.
</DialogDescription>
</DialogHeader>
<Alert className="bg-amber-50 border-amber-200">
<AlertDescription className="text-amber-900">
<strong>Importante:</strong> Anote ou copie estas credenciais agora. Por segurança, essa senha não será exibida novamente.
</AlertDescription>
</Alert>
<Alert className="bg-blue-50 border-blue-200">
<AlertDescription className="text-blue-900">
<strong>📧 Confirme o email:</strong> Um email de confirmação foi enviado para <strong>{email}</strong>.
O {userType} deve clicar no link de confirmação antes de fazer o primeiro login.
</AlertDescription>
</Alert>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="email">Email de Acesso</Label>
<div className="flex gap-2">
<Input
id="email"
value={email}
readOnly
className="bg-muted"
/>
<Button
type="button"
variant="outline"
size="icon"
onClick={handleCopyEmail}
title="Copiar email"
>
{copiedEmail ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="password">Senha Temporária</Label>
<div className="flex gap-2">
<div className="relative flex-1">
<Input
id="password"
type={showPassword ? "text" : "password"}
value={password}
readOnly
className="bg-muted pr-10"
/>
<Button
type="button"
variant="ghost"
size="icon"
className="absolute right-0 top-0 h-full"
onClick={() => setShowPassword(!showPassword)}
title={showPassword ? "Ocultar senha" : "Mostrar senha"}
>
{showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</Button>
</div>
<Button
type="button"
variant="outline"
size="icon"
onClick={handleCopyPassword}
title="Copiar senha"
>
{copiedPassword ? <CheckCircle2 className="h-4 w-4 text-green-500" /> : <Copy className="h-4 w-4" />}
</Button>
</div>
</div>
</div>
<div className="bg-blue-50 border border-blue-200 rounded-md p-3 text-sm text-blue-900">
<strong>Próximos passos:</strong>
<ol className="list-decimal list-inside mt-2 space-y-1">
<li>Compartilhe estas credenciais com o {userType}</li>
<li>
<strong className="text-blue-700">O {userType} deve confirmar o email</strong> clicando no link enviado para{" "}
<strong>{email}</strong> (verifique também a pasta de spam)
</li>
<li>
Após confirmar o email, o {userType} deve acessar:{" "}
<code className="bg-blue-100 px-1 py-0.5 rounded text-xs font-mono">
{userType === "médico" ? "/login" : "/login-paciente"}
</code>
</li>
<li>
Após o login, terá acesso à área:{" "}
<code className="bg-blue-100 px-1 py-0.5 rounded text-xs font-mono">
{userType === "médico" ? "/profissional" : "/paciente"}
</code>
</li>
<li>Recomende trocar a senha no primeiro acesso</li>
</ol>
</div>
<DialogFooter className="flex-col sm:flex-row gap-2">
<Button
type="button"
variant="outline"
onClick={handleCopyBoth}
className="w-full sm:w-auto"
>
<Copy className="mr-2 h-4 w-4" />
Copiar Tudo
</Button>
<Button
type="button"
onClick={() => onOpenChange(false)}
className="w-full sm:w-auto"
>
Fechar
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -7,6 +7,7 @@ import { Input } from "@/components/ui/input"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { useState, useEffect, useRef } from "react"
import { SidebarTrigger } from "../ui/sidebar"
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
const { logout, user } = useAuth();
@ -44,7 +45,12 @@ export function PagesHeader({ title = "", subtitle = "" }: { title?: string, sub
<Bell className="h-4 w-4" />
</Button>
<SimpleThemeToggle />
<Button
variant="outline"
className="text-primary border-primary bg-transparent shadow-sm shadow-blue-500/10 border border-blue-200 hover:bg-blue-50 dark:shadow-none dark:border-primary dark:hover:bg-primary dark:hover:text-primary-foreground"
asChild
></Button>
{/* Avatar Dropdown Simples */}
<div className="relative" ref={dropdownRef}>
<Button

View File

@ -24,10 +24,13 @@ import {
removerAnexoMedico,
MedicoInput, // 👈 importado do lib/api
Medico, // 👈 adicionado import do tipo Medico
criarUsuarioMedico,
CreateUserWithPasswordResponse,
} from "@/lib/api";
;
import { buscarCepAPI } from "@/lib/api";
import { buscarCepAPI } from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
type FormacaoAcademica = {
instituicao: string;
@ -150,6 +153,11 @@ export function DoctorRegistrationForm({
const [isSearchingCEP, setSearchingCEP] = useState(false);
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
// Estados para o dialog de credenciais
const [showCredentials, setShowCredentials] = useState(false);
const [credentials, setCredentials] = useState<CreateUserWithPasswordResponse | null>(null);
const [savedDoctor, setSavedDoctor] = useState<Medico | null>(null);
const title = useMemo(() => (mode === "create" ? "Cadastro de Médico" : "Editar Médico"), [mode]);
@ -391,8 +399,69 @@ if (missingFields.length > 0) {
console.log("✅ Médico salvo com sucesso:", saved);
onSaved?.(saved);
setSubmitting(false);
// Se for criação de novo médico e tiver email válido, cria usuário
if (mode === "create" && form.email && form.email.includes('@')) {
console.log("🔐 Iniciando criação de usuário para o médico...");
console.log("📧 Email:", form.email);
console.log("👤 Nome:", form.full_name);
console.log("📱 Telefone:", form.celular);
try {
const userCredentials = await criarUsuarioMedico({
email: form.email,
full_name: form.full_name,
phone_mobile: form.celular,
});
console.log("✅ Usuário criado com sucesso!", userCredentials);
console.log("🔑 Senha gerada:", userCredentials.password);
// Armazena as credenciais e mostra o dialog
setCredentials(userCredentials);
setShowCredentials(true);
setSavedDoctor(saved); // Salva médico para chamar onSaved depois
console.log("📋 Credenciais definidas, dialog deve aparecer!");
// NÃO chama onSaved aqui! Isso fecha o formulário.
// O dialog vai chamar onSaved quando o usuário fechar
setSubmitting(false);
return; // ← IMPORTANTE: Impede que o código abaixo seja executado
} catch (userError: any) {
console.error("❌ ERRO ao criar usuário:", userError);
console.error("📋 Stack trace:", userError?.stack);
const errorMessage = userError?.message || "Erro desconhecido";
console.error("💬 Mensagem:", errorMessage);
// Mostra erro mas fecha o formulário normalmente
alert(`Médico cadastrado com sucesso!\n\n⚠ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`);
// Fecha o formulário mesmo com erro na criação de usuário
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
onSaved?.(saved);
if (inline) onClose?.();
else onOpenChange?.(false);
setSubmitting(false);
return;
}
} else {
console.log("⚠️ Não criará usuário. Motivo:");
console.log(" - Mode:", mode);
console.log(" - Email:", form.email);
console.log(" - Tem @:", form.email?.includes('@'));
// Se não for criar usuário, fecha normalmente
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
onSaved?.(saved);
if (inline) onClose?.();
else onOpenChange?.(false);
setSubmitting(false);
}
} catch (err: any) {
console.error("❌ Erro ao salvar médico:", err);
console.error("❌ Detalhes do erro:", {
@ -935,18 +1004,90 @@ if (missingFields.length > 0) {
</>
);
if (inline) return <div className="space-y-6">{content}</div>;
if (inline) {
return (
<>
<div className="space-y-6">{content}</div>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentials}
onOpenChange={(open) => {
console.log("🔄 CredentialsDialog (inline) onOpenChange:", open);
setShowCredentials(open);
if (!open) {
console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário");
// Chama onSaved com o médico salvo
if (savedDoctor) {
console.log("✅ Chamando onSaved com médico:", savedDoctor.id);
onSaved?.(savedDoctor);
}
// Limpa o formulário e fecha
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
setSavedDoctor(null);
onClose?.();
}
}}
email={credentials.email}
password={credentials.password}
userName={form.full_name}
userType="médico"
/>
)}
</>
);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" /> {title}
</DialogTitle>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
<>
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" /> {title}
</DialogTitle>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentials}
onOpenChange={(open) => {
console.log("🔄 CredentialsDialog (dialog mode) onOpenChange:", open);
setShowCredentials(open);
if (!open) {
console.log("🔄 Dialog fechando - chamando onSaved e fechando modal principal");
// Chama onSaved com o médico salvo
if (savedDoctor) {
console.log("✅ Chamando onSaved com médico:", savedDoctor.id);
onSaved?.(savedDoctor);
}
// Limpa o formulário e fecha o modal principal
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
setSavedDoctor(null);
onOpenChange?.(false);
}
}}
email={credentials.email}
password={credentials.password}
userName={form.full_name}
userType="médico"
/>
)}
</>
);
}

View File

@ -25,10 +25,13 @@ import {
listarAnexos,
removerAnexo,
buscarPacientePorId,
criarUsuarioPaciente,
CreateUserWithPasswordResponse,
} from "@/lib/api";
import { validarCPFLocal } from "@/lib/utils";
import { verificarCpfDuplicado } from "@/lib/api";
import { verificarCpfDuplicado } from "@/lib/api";
import { CredentialsDialog } from "@/components/credentials-dialog";
@ -104,6 +107,11 @@ export function PatientRegistrationForm({
const [isSearchingCEP, setSearchingCEP] = useState(false);
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
const [serverAnexos, setServerAnexos] = useState<any[]>([]);
// Estados para o dialog de credenciais
const [showCredentials, setShowCredentials] = useState(false);
const [credentials, setCredentials] = useState<CreateUserWithPasswordResponse | null>(null);
const [savedPatient, setSavedPatient] = useState<Paciente | null>(null);
const title = useMemo(() => (mode === "create" ? "Cadastro de Paciente" : "Editar Paciente"), [mode]);
@ -264,15 +272,88 @@ export function PatientRegistrationForm({
}
}
// Se for criação de novo paciente e tiver email válido, cria usuário
if (mode === "create" && form.email && form.email.includes('@')) {
console.log("🔐 Iniciando criação de usuário para o paciente...");
console.log("📧 Email:", form.email);
console.log("👤 Nome:", form.nome);
console.log("📱 Telefone:", form.telefone);
try {
const userCredentials = await criarUsuarioPaciente({
email: form.email,
full_name: form.nome,
phone_mobile: form.telefone,
});
console.log("✅ Usuário criado com sucesso!", userCredentials);
console.log("🔑 Senha gerada:", userCredentials.password);
// Armazena as credenciais e mostra o dialog
console.log("📋 Antes de setCredentials - credentials atual:", credentials);
console.log("📋 Antes de setShowCredentials - showCredentials atual:", showCredentials);
setCredentials(userCredentials);
setShowCredentials(true);
console.log("📋 Depois de set - credentials:", userCredentials);
console.log("📋 Depois de set - showCredentials: true");
console.log("📋 Modo inline?", inline);
console.log("📋 userCredentials completo:", JSON.stringify(userCredentials));
// Força re-render
setTimeout(() => {
console.log("⏰ Timeout - credentials:", credentials);
console.log("⏰ Timeout - showCredentials:", showCredentials);
}, 100);
console.log("📋 Credenciais definidas, dialog deve aparecer!");
// Salva o paciente para chamar onSaved depois
setSavedPatient(saved);
// ⚠️ NÃO chama onSaved aqui! O dialog vai chamar quando fechar.
// Se chamar agora, o formulário fecha e o dialog desaparece.
console.log("⚠️ NÃO chamando onSaved ainda - aguardando dialog fechar");
// RETORNA AQUI para não executar o código abaixo
return;
} catch (userError: any) {
console.error("❌ ERRO ao criar usuário:", userError);
console.error("📋 Stack trace:", userError?.stack);
const errorMessage = userError?.message || "Erro desconhecido";
console.error("<22> Mensagem:", errorMessage);
// Mostra erro mas fecha o formulário normalmente
alert(`Paciente cadastrado com sucesso!\n\n⚠ Porém, houve erro ao criar usuário de acesso:\n${errorMessage}\n\nVerifique os logs do console (F12) para mais detalhes.`);
// Fecha o formulário mesmo com erro na criação de usuário
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
}
} else {
console.log("⚠️ Não criará usuário. Motivo:");
console.log(" - Mode:", mode);
console.log(" - Email:", form.email);
console.log(" - Tem @:", form.email?.includes('@'));
// Se não for criar usuário, fecha normalmente
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!");
}
onSaved?.(saved);
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
if (inline) onClose?.();
else onOpenChange?.(false);
alert(mode === "create" ? "Paciente cadastrado!" : "Paciente atualizado!");
} catch (err: any) {
setErrors({ submit: err?.message || "Erro ao salvar paciente." });
} finally {
@ -611,18 +692,85 @@ export function PatientRegistrationForm({
</>
);
if (inline) return <div className="space-y-6">{content}</div>;
if (inline) {
return (
<>
<div className="space-y-6">{content}</div>
{/* Debug */}
{console.log("🎨 RENDER inline - credentials:", credentials, "showCredentials:", showCredentials)}
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentials}
onOpenChange={(open) => {
console.log("🔄 CredentialsDialog onOpenChange:", open);
setShowCredentials(open);
if (!open) {
console.log("🔄 Dialog fechando - chamando onSaved e limpando formulário");
// Chama onSaved com o paciente salvo
if (savedPatient) {
console.log("✅ Chamando onSaved com paciente:", savedPatient.id);
onSaved?.(savedPatient);
}
// Limpa o formulário e fecha
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
setSavedPatient(null);
onClose?.();
}
}}
email={credentials.email}
password={credentials.password}
userName={form.nome}
userType="paciente"
/>
)}
</>
);
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" /> {title}
</DialogTitle>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
<>
{console.log("🎨 RENDER dialog - credentials:", credentials, "showCredentials:", showCredentials)}
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<User className="h-5 w-5" /> {title}
</DialogTitle>
</DialogHeader>
{content}
</DialogContent>
</Dialog>
{/* Dialog de credenciais */}
{credentials && (
<CredentialsDialog
open={showCredentials}
onOpenChange={(open) => {
setShowCredentials(open);
if (!open) {
// Quando fechar o dialog, limpa o formulário e fecha o modal principal
setForm(initial);
setPhotoPreview(null);
setServerAnexos([]);
setCredentials(null);
onOpenChange?.(false);
}
}}
email={credentials.email}
password={credentials.password}
userName={form.nome}
userType="paciente"
/>
)}
</>
);
}

View File

@ -5,31 +5,84 @@ import eslint from "@eslint/js";
import nextPlugin from "@next/eslint-plugin-next";
import unicornPlugin from "eslint-plugin-unicorn";
import prettierConfig from "eslint-config-prettier";
import { FlatCompat } from "@eslint/eslintrc";
import { fileURLToPath } from "url";
import { dirname } from "path";
export default [
eslint.configs.recommended,
...tseslint.configs.recommended,
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname
});
const eslintConfig = [
{
ignores: ["node_modules/**", ".next/**", "out/**", "build/**", "next-env.d.ts"],
},
// Base JS/TS config
{
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
plugins: {
"@next/next": nextPlugin,
"unicorn": unicornPlugin,
},
languageOptions: {
globals: {
...globals.browser,
...globals.node,
},
parser: tseslint.parser,
parserOptions: {
project: "./tsconfig.json",
},
globals: {
...globals.browser,
...globals.node,
},
},
plugins: {
"@next/next": nextPlugin,
},
rules: {
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs["core-web-vitals"].rules,
...unicornPlugin.configs.recommended.rules,
...nextPlugin.configs.recommended.rules,
...nextPlugin.configs["core-web-vitals"].rules,
}
},
// TypeScript specific config
{
files: ["**/*.{ts,tsx}"],
plugins: {
"unicorn": unicornPlugin,
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
project: "./tsconfig.json",
},
},
rules: {
...tseslint.configs.recommended.rules,
...unicornPlugin.configs.recommended.rules,
// Disable noisy unicorn rules
"unicorn/prevent-abbreviations": "off",
"unicorn/filename-case": "off",
"unicorn/no-null": "off",
"unicorn/consistent-function-scoping": "off",
"unicorn/no-array-for-each": "off",
"unicorn/catch-error-name": "off",
"unicorn/explicit-length-check": "off",
"unicorn/no-array-reduce": "off",
"unicorn/prefer-spread": "off",
"unicorn/no-document-cookie": "off",
"unicorn/prefer-query-selector": "off",
"unicorn/prefer-add-event-listener": "off",
"unicorn/prefer-string-slice": "off",
"unicorn/prefer-string-replace-all": "off",
"unicorn/prefer-number-properties": "off",
"unicorn/consistent-existence-index-check": "off",
"unicorn/no-negated-condition": "off",
"unicorn/switch-case-braces": "off",
"unicorn/prefer-global-this": "off",
"unicorn/no-useless-undefined": "off",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-sort": "off",
"unicorn/numeric-separators-style": "off",
"unicorn/prefer-optional-catch-binding": "off",
"unicorn/prefer-ternary": "off",
"unicorn/prefer-code-point": "off",
"unicorn/prefer-single-call": "off",
}
},
prettierConfig,
];
...compat.extends("next/core-web-vitals"),
];
export default eslintConfig;

View File

@ -1,5 +1,8 @@
// lib/api.ts
import { ENV_CONFIG } from '@/lib/env-config';
import { API_KEY } from '@/lib/config';
export type ApiOk<T = any> = {
success?: boolean;
data: T;
@ -194,7 +197,32 @@ async function parse<T>(res: Response): Promise<T> {
console.error("[API ERROR]", res.url, res.status, json);
const code = (json && (json.error?.code || json.code)) ?? res.status;
const msg = (json && (json.error?.message || json.message)) ?? res.statusText;
throw new Error(`${code}: ${msg}`);
// Mensagens amigáveis para erros comuns
let friendlyMessage = `${code}: ${msg}`;
// Erro de CPF duplicado
if (code === '23505' && msg.includes('patients_cpf_key')) {
friendlyMessage = 'Já existe um paciente cadastrado com este CPF. Por favor, verifique se o paciente já está registrado no sistema ou use um CPF diferente.';
}
// Erro de email duplicado (paciente)
else if (code === '23505' && msg.includes('patients_email_key')) {
friendlyMessage = 'Já existe um paciente cadastrado com este email. Por favor, use um email diferente.';
}
// Erro de CRM duplicado (médico)
else if (code === '23505' && msg.includes('doctors_crm')) {
friendlyMessage = 'Já existe um médico cadastrado com este CRM. Por favor, verifique se o médico já está registrado no sistema.';
}
// Erro de email duplicado (médico)
else if (code === '23505' && msg.includes('doctors_email_key')) {
friendlyMessage = 'Já existe um médico cadastrado com este email. Por favor, use um email diferente.';
}
// Outros erros de constraint unique
else if (code === '23505') {
friendlyMessage = 'Registro duplicado: já existe um cadastro com essas informações no sistema.';
}
throw new Error(friendlyMessage);
}
return (json?.data ?? json) as T;
@ -552,6 +580,432 @@ export async function excluirMedico(id: string | number): Promise<void> {
await parse<any>(res);
}
// ===== USUÁRIOS =====
export type UserRole = {
id: string;
user_id: string;
role: string;
created_at: string;
};
export async function listarUserRoles(): Promise<UserRole[]> {
const url = `https://mock.apidog.com/m1/1053378-0-default/rest/v1/user_roles`;
const res = await fetch(url, {
method: "GET",
headers: baseHeaders(),
});
return await parse<UserRole[]>(res);
}
export type User = {
id: string;
email: string;
email_confirmed_at: string;
created_at: string;
last_sign_in_at: string;
};
export type CurrentUser = {
id: string;
email: string;
email_confirmed_at: string;
created_at: string;
last_sign_in_at: string;
};
export type Profile = {
id: string;
full_name: string;
email: string;
phone: string;
avatar_url: string;
disabled: boolean;
created_at: string;
updated_at: string;
};
export type ProfileInput = Partial<Omit<Profile, 'id' | 'created_at' | 'updated_at'>>;
export type Permissions = {
isAdmin: boolean;
isManager: boolean;
isDoctor: boolean;
isSecretary: boolean;
isAdminOrManager: boolean;
};
export type UserInfo = {
user: User;
profile: Profile;
roles: string[];
permissions: Permissions;
};
export async function getCurrentUser(): Promise<CurrentUser> {
const url = `https://mock.apidog.com/m1/1053378-0-default/auth/v1/user`;
const res = await fetch(url, {
method: "GET",
headers: baseHeaders(),
});
return await parse<CurrentUser>(res);
}
export async function getUserInfo(): Promise<UserInfo> {
const url = `https://mock.apidog.com/m1/1053378-0-default/functions/v1/user-info`;
const res = await fetch(url, {
method: "GET",
headers: baseHeaders(),
});
return await parse<UserInfo>(res);
}
export type CreateUserInput = {
email: string;
full_name: string;
phone: string;
role: string;
password?: string;
};
export type CreatedUser = {
id: string;
email: string;
full_name: string;
phone: string;
role: string;
};
export type CreateUserResponse = {
success: boolean;
user: CreatedUser;
password?: string;
};
export type CreateUserWithPasswordResponse = {
success: boolean;
user: CreatedUser;
email: string;
password: string;
};
// Função para gerar senha aleatória (formato: senhaXXX!)
export 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}!`;
}
export async function criarUsuario(input: CreateUserInput): Promise<CreateUserResponse> {
const url = `https://mock.apidog.com/m1/1053378-0-default/functions/v1/create-user`;
const res = await fetch(url, {
method: "POST",
headers: { ...baseHeaders(), "Content-Type": "application/json" },
body: JSON.stringify(input),
});
return await parse<CreateUserResponse>(res);
}
// ============================================
// 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<CreateUserWithPasswordResponse> {
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": API_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<CreateUserWithPasswordResponse> {
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": API_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<{
logradouro?: string;
@ -588,3 +1042,14 @@ export async function adicionarAnexoMedico(_id: string | number, _file: File): P
export async function removerAnexoMedico(_id: string | number, _anexoId: string | number): Promise<void> {}
export async function uploadFotoMedico(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { return {}; }
export async function removerFotoMedico(_id: string | number): Promise<void> {}
// ===== PERFIS DE USUÁRIOS =====
export async function listarPerfis(): Promise<Profile[]> {
const url = `https://mock.apidog.com/m1/1053378-0-default/rest/v1/profiles`;
const res = await fetch(url, {
method: "GET",
headers: baseHeaders(),
});
return await parse<Profile[]>(res);
}

View File

@ -103,6 +103,11 @@ export async function loginUser(
payload,
timestamp: new Date().toLocaleTimeString()
});
console.log('🔑 [AUTH-API] Credenciais sendo usadas no login:');
console.log('📧 Email:', email);
console.log('🔐 Senha:', password);
console.log('👤 UserType:', userType);
// Delay para visualizar na aba Network
await new Promise(resolve => setTimeout(resolve, 50));
@ -113,53 +118,19 @@ export async function loginUser(
// Debug: Log request sem credenciais sensíveis
debugRequest('POST', url, getLoginHeaders(), payload);
let response = await fetch(url, {
const response = await fetch(url, {
method: 'POST',
headers: getLoginHeaders(),
body: JSON.stringify(payload),
});
// Se login falhar com 400, tentar criar usuário automaticamente
if (!response.ok && response.status === 400) {
console.log('[AUTH-API] Login falhou (400), tentando criar usuário...');
const signupUrl = `${ENV_CONFIG.SUPABASE_URL}/auth/v1/signup`;
const signupPayload = {
email,
password,
data: {
userType: userType,
name: email.split('@')[0],
}
};
debugRequest('POST', signupUrl, getLoginHeaders(), signupPayload);
const signupResponse = await fetch(signupUrl, {
method: 'POST',
headers: getLoginHeaders(),
body: JSON.stringify(signupPayload),
});
if (signupResponse.ok) {
console.log('[AUTH-API] Usuário criado, tentando login novamente...');
await new Promise(resolve => setTimeout(resolve, 100));
response = await fetch(url, {
method: 'POST',
headers: getLoginHeaders(),
body: JSON.stringify(payload),
});
}
}
console.log(`[AUTH-API] Login response: ${response.status} ${response.statusText}`, {
url: response.url,
status: response.status,
timestamp: new Date().toLocaleTimeString()
});
// Se ainda for 400, mostrar detalhes do erro
// Se falhar, mostrar detalhes do erro
if (!response.ok) {
try {
const errorText = await response.text();

1
susconecta/lib/report.ts Normal file
View File

@ -0,0 +1 @@

View File

@ -6,7 +6,7 @@
"scripts": {
"build": "next build",
"dev": "next dev",
"lint": "next lint",
"lint": "eslint .",
"start": "next start"
},
"dependencies": {
@ -86,6 +86,7 @@
"tailwindcss": "^4.1.9",
"tw-animate-css": "1.3.3",
"typescript": "^5",
"typescript-eslint": "^8.45.0"
"typescript-eslint": "^8.45.0",
"@eslint/eslintrc": "^3"
}
}

View File

@ -183,6 +183,9 @@ importers:
specifier: 3.25.67
version: 3.25.67
devDependencies:
'@eslint/eslintrc':
specifier: ^3
version: 3.3.1
'@eslint/js':
specifier: ^9.36.0
version: 9.36.0