feature(refactor): reorganize project structure and routes
This commit is contained in:
parent
b50050a545
commit
498d6c80c1
@ -1,19 +1,19 @@
|
|||||||
// eslint.config.js
|
// eslint.config.js
|
||||||
import globals from "globals";
|
import globals from 'globals';
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from 'typescript-eslint';
|
||||||
import eslint from "@eslint/js";
|
import eslint from '@eslint/js';
|
||||||
import nextPlugin from "@next/eslint-plugin-next";
|
import nextPlugin from '@next/eslint-plugin-next';
|
||||||
import unicornPlugin from "eslint-plugin-unicorn";
|
import unicornPlugin from 'eslint-plugin-unicorn';
|
||||||
import prettierConfig from "eslint-config-prettier";
|
import prettierConfig from 'eslint-config-prettier';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
eslint.configs.recommended,
|
eslint.configs.recommended,
|
||||||
...tseslint.configs.recommended,
|
...tseslint.configs.recommended,
|
||||||
{
|
{
|
||||||
files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"],
|
files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'],
|
||||||
plugins: {
|
plugins: {
|
||||||
"@next/next": nextPlugin,
|
'@next/next': nextPlugin,
|
||||||
"unicorn": unicornPlugin,
|
unicorn: unicornPlugin,
|
||||||
},
|
},
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
globals: {
|
globals: {
|
||||||
@ -22,14 +22,53 @@ export default [
|
|||||||
},
|
},
|
||||||
parser: tseslint.parser,
|
parser: tseslint.parser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: "./tsconfig.json",
|
project: './tsconfig.json',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...nextPlugin.configs.recommended.rules,
|
...nextPlugin.configs.recommended.rules,
|
||||||
...nextPlugin.configs["core-web-vitals"].rules,
|
...nextPlugin.configs['core-web-vitals'].rules,
|
||||||
...unicornPlugin.configs.recommended.rules,
|
'unicorn/prevent-abbreviations': 'off',
|
||||||
}
|
'unicorn/no-null': 'off',
|
||||||
|
'unicorn/prefer-string-replace-all': 'off',
|
||||||
|
'unicorn/prefer-string-slice': 'off',
|
||||||
|
'unicorn/prefer-number-properties': 'off',
|
||||||
|
'unicorn/no-array-reduce': 'off',
|
||||||
|
'unicorn/no-array-for-each': 'off',
|
||||||
|
'unicorn/prefer-global-this': 'off',
|
||||||
|
'unicorn/no-useless-undefined': 'off',
|
||||||
|
'unicorn/explicit-length-check': 'off',
|
||||||
|
'unicorn/consistent-existence-index-check': 'off',
|
||||||
|
'unicorn/prefer-ternary': 'off',
|
||||||
|
'unicorn/numeric-separators-style': 'off',
|
||||||
|
'unicorn/filename-case': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
cases: {
|
||||||
|
camelCase: true,
|
||||||
|
pascalCase: true,
|
||||||
|
kebabCase: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'unicorn/prefer-add-event-listener': 'off',
|
||||||
|
'unicorn/prefer-spread': 'off',
|
||||||
|
'unicorn/consistent-function-scoping': 'off',
|
||||||
|
'unicorn/no-document-cookie': 'off',
|
||||||
|
'unicorn/no-negated-condition': 'off',
|
||||||
|
'unicorn/prefer-code-point': 'off',
|
||||||
|
'unicorn/prefer-single-call': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
varsIgnorePattern: '^_',
|
||||||
|
caughtErrorsIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'prefer-const': 'off',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
prettierConfig,
|
prettierConfig,
|
||||||
];
|
];
|
||||||
49
package-lock.json
generated
49
package-lock.json
generated
@ -69,6 +69,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@next/eslint-plugin-next": "^15.5.4",
|
||||||
"@tailwindcss/postcss": "^4.1.9",
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
@ -79,6 +80,7 @@
|
|||||||
"eslint-config-next": "^15.5.4",
|
"eslint-config-next": "^15.5.4",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-unicorn": "^61.0.2",
|
"eslint-plugin-unicorn": "^61.0.2",
|
||||||
|
"globals": "^16.4.0",
|
||||||
"next": "^15.5.4",
|
"next": "^15.5.4",
|
||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
"tailwindcss": "^4.1.9",
|
"tailwindcss": "^4.1.9",
|
||||||
@ -284,6 +286,19 @@
|
|||||||
"concat-map": "0.0.1"
|
"concat-map": "0.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@eslint/eslintrc/node_modules/globals": {
|
||||||
|
"version": "14.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
||||||
|
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@eslint/eslintrc/node_modules/ignore": {
|
"node_modules/@eslint/eslintrc/node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
@ -5399,19 +5414,6 @@
|
|||||||
"eslint": ">=9.29.0"
|
"eslint": ">=9.29.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
|
|
||||||
"version": "16.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
|
|
||||||
"integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
"version": "8.4.0",
|
"version": "8.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
||||||
@ -5917,9 +5919,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "14.0.0",
|
"version": "16.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
|
||||||
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
|
"integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -9402,21 +9404,6 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/@next/swc-win32-ia32-msvc": {
|
|
||||||
"version": "14.2.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.16.tgz",
|
|
||||||
"integrity": "sha512-jhPl3nN0oKEshJBNDAo0etGMzv0j3q3VYorTSFqH1o3rwv1MQRdor27u1zhkgsHPNeY1jxcgyx1ZsCkDD1IHgg==",
|
|
||||||
"cpu": [
|
|
||||||
"ia32"
|
|
||||||
],
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -71,6 +71,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.36.0",
|
"@eslint/js": "^9.36.0",
|
||||||
|
"@next/eslint-plugin-next": "^15.5.4",
|
||||||
"@tailwindcss/postcss": "^4.1.9",
|
"@tailwindcss/postcss": "^4.1.9",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
@ -81,6 +82,7 @@
|
|||||||
"eslint-config-next": "^15.5.4",
|
"eslint-config-next": "^15.5.4",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-unicorn": "^61.0.2",
|
"eslint-plugin-unicorn": "^61.0.2",
|
||||||
|
"globals": "^16.4.0",
|
||||||
"next": "^15.5.4",
|
"next": "^15.5.4",
|
||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
"tailwindcss": "^4.1.9",
|
"tailwindcss": "^4.1.9",
|
||||||
|
|||||||
6
pnpm-lock.yaml
generated
6
pnpm-lock.yaml
generated
@ -186,6 +186,9 @@ importers:
|
|||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.36.0
|
specifier: ^9.36.0
|
||||||
version: 9.36.0
|
version: 9.36.0
|
||||||
|
'@next/eslint-plugin-next':
|
||||||
|
specifier: ^15.5.4
|
||||||
|
version: 15.5.4
|
||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.1.9
|
specifier: ^4.1.9
|
||||||
version: 4.1.13
|
version: 4.1.13
|
||||||
@ -216,6 +219,9 @@ importers:
|
|||||||
eslint-plugin-unicorn:
|
eslint-plugin-unicorn:
|
||||||
specifier: ^61.0.2
|
specifier: ^61.0.2
|
||||||
version: 61.0.2(eslint@9.36.0(jiti@2.5.1))
|
version: 61.0.2(eslint@9.36.0(jiti@2.5.1))
|
||||||
|
globals:
|
||||||
|
specifier: ^16.4.0
|
||||||
|
version: 16.4.0
|
||||||
next:
|
next:
|
||||||
specifier: ^15.5.4
|
specifier: ^15.5.4
|
||||||
version: 15.5.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
version: 15.5.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||||
|
|||||||
@ -8,8 +8,6 @@ import dayGridPlugin from "@fullcalendar/daygrid";
|
|||||||
import interactionPlugin from "@fullcalendar/interaction";
|
import interactionPlugin from "@fullcalendar/interaction";
|
||||||
import timeGridPlugin from "@fullcalendar/timegrid";
|
import timeGridPlugin from "@fullcalendar/timegrid";
|
||||||
import { EventInput } from "@fullcalendar/core/index.js";
|
import { EventInput } from "@fullcalendar/core/index.js";
|
||||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
|
||||||
import { PagesHeader } from "@/components/dashboard/header";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
mockAppointments,
|
mockAppointments,
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
import ProtectedRoute from "@/components/layout/ProtectedRoute";
|
||||||
import { Sidebar } from "@/components/dashboard/sidebar";
|
import { Sidebar } from "@/components/layout/app/Sidebar";
|
||||||
import { SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
|
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||||
import { PagesHeader } from "@/components/dashboard/header";
|
import { PagesHeader } from "@/components/layout/app/Header";
|
||||||
|
|
||||||
export default function MainRoutesLayout({
|
export default function MainRoutesLayout({
|
||||||
children,
|
children,
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Header } from "@/components/header"
|
import { Header } from "@/components/layout/marketing/Header"
|
||||||
import { AboutSection } from "@/components/about-section"
|
import { AboutSection } from "@/components/about-section"
|
||||||
import { Footer } from "@/components/footer"
|
import { Footer } from "@/components/layout/marketing/Footer"
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutPage() {
|
||||||
return (
|
return (
|
||||||
11
src/app/(paciente)/layout.tsx
Normal file
11
src/app/(paciente)/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import ProtectedRoute from '@/components/layout/ProtectedRoute'
|
||||||
|
|
||||||
|
export default function PacienteLayout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute requiredUserType={['paciente']}>
|
||||||
|
{children}
|
||||||
|
</ProtectedRoute>
|
||||||
|
)
|
||||||
|
}
|
||||||
92
src/app/(paciente)/paciente/page.tsx
Normal file
92
src/app/(paciente)/paciente/page.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
'use client'
|
||||||
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { User, LogOut, Home } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
export default function PacientePage() {
|
||||||
|
const { logout, user } = useAuth()
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
console.log('[PACIENTE] Iniciando logout...')
|
||||||
|
await logout()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<Link href="/">
|
||||||
|
<Home className="h-4 w-4" />
|
||||||
|
Voltar ao Início
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
|
||||||
|
{/* 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>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
src/app/(profissional)/layout.tsx
Normal file
11
src/app/(profissional)/layout.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import ProtectedRoute from '@/components/layout/ProtectedRoute'
|
||||||
|
|
||||||
|
export default function ProfissionalLayout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ProtectedRoute requiredUserType={['profissional']}>
|
||||||
|
{children}
|
||||||
|
</ProtectedRoute>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -3,7 +3,6 @@
|
|||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import SignatureCanvas from "react-signature-canvas";
|
import SignatureCanvas from "react-signature-canvas";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
|
||||||
import { useAuth } from "@/hooks/useAuth";
|
import { useAuth } from "@/hooks/useAuth";
|
||||||
import { buscarPacientes } from "@/lib/api";
|
import { buscarPacientes } from "@/lib/api";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@ -3251,8 +3250,7 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtectedRoute requiredUserType={["profissional"]}>
|
<div className="container mx-auto px-4 py-8">
|
||||||
<div className="container mx-auto px-4 py-8">
|
|
||||||
<header className="bg-card shadow-md rounded-lg border border-border p-4 mb-6 flex items-center justify-between">
|
<header className="bg-card shadow-md rounded-lg border border-border p-4 mb-6 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Avatar className="h-12 w-12">
|
<Avatar className="h-12 w-12">
|
||||||
@ -3510,7 +3508,6 @@ Nevo melanocítico benigno. Seguimento clínico recomendado.
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ProtectedRoute>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
3514
src/app/(profissional)/profissional/page.tsx
Normal file
3514
src/app/(profissional)/profissional/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
@ -56,6 +56,7 @@ export default function NovoAgendamentoPage() {
|
|||||||
/>
|
/>
|
||||||
</main>
|
</main>
|
||||||
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
|
<FooterAgenda onSave={handleSave} onCancel={handleCancel} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import type React from "react"
|
import type React from "react"
|
||||||
import type { Metadata } from "next"
|
import type { Metadata } from "next"
|
||||||
import { AuthProvider } from "@/hooks/useAuth"
|
import { AuthProvider } from "@/hooks/useAuth"
|
||||||
import { ThemeProvider } from "@/components/theme-provider"
|
import { ThemeProvider } from "@/components/layout/ThemeProvider"
|
||||||
import "./globals.css"
|
import "./globals.css"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
|
|||||||
@ -1,124 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
||||||
import { AuthenticationError } from '@/lib/auth'
|
|
||||||
|
|
||||||
export default function LoginAdminPage() {
|
|
||||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const router = useRouter()
|
|
||||||
const { login } = useAuth()
|
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setLoading(true)
|
|
||||||
setError('')
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Tentar fazer login usando o contexto com tipo administrador
|
|
||||||
const success = await login(credentials.email, credentials.password, 'administrador')
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
console.log('[LOGIN-ADMIN] Login bem-sucedido, redirecionando...')
|
|
||||||
|
|
||||||
// Redirecionamento direto - solução que funcionou
|
|
||||||
window.location.href = '/dashboard'
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[LOGIN-ADMIN] Erro no login:', err)
|
|
||||||
|
|
||||||
if (err instanceof AuthenticationError) {
|
|
||||||
setError(err.message)
|
|
||||||
} else {
|
|
||||||
setError('Erro inesperado. Tente novamente.')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="max-w-md w-full space-y-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="mt-6 text-3xl font-extrabold text-foreground">
|
|
||||||
Login Administrador de Clínica
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
|
||||||
Entre com suas credenciais para acessar o sistema administrativo
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-center">Acesso Administrativo</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form onSubmit={handleLogin} className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="Digite seu email"
|
|
||||||
value={credentials.email}
|
|
||||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
|
||||||
required
|
|
||||||
className="mt-1"
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
|
||||||
Senha
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Digite sua senha"
|
|
||||||
value={credentials.password}
|
|
||||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
|
||||||
required
|
|
||||||
className="mt-1"
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? 'Entrando...' : 'Entrar no Sistema Administrativo'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
|
||||||
<Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200">
|
|
||||||
<Link href="/">
|
|
||||||
Voltar ao Início
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,122 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { useState } from 'react'
|
|
||||||
import { useRouter } from 'next/navigation'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Input } from '@/components/ui/input'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
||||||
import { AuthenticationError } from '@/lib/auth'
|
|
||||||
|
|
||||||
export default function LoginPacientePage() {
|
|
||||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const router = useRouter()
|
|
||||||
const { login } = useAuth()
|
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
setLoading(true)
|
|
||||||
setError('')
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Tentar fazer login usando o contexto com tipo paciente
|
|
||||||
const success = await login(credentials.email, credentials.password, 'paciente')
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
// Redirecionar para a página do paciente
|
|
||||||
router.push('/paciente')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[LOGIN-PACIENTE] Erro no login:', err)
|
|
||||||
|
|
||||||
if (err instanceof AuthenticationError) {
|
|
||||||
setError(err.message)
|
|
||||||
} else {
|
|
||||||
setError('Erro inesperado. Tente novamente.')
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-background py-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="max-w-md w-full space-y-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="mt-6 text-3xl font-extrabold text-foreground">
|
|
||||||
Sou Paciente
|
|
||||||
</h2>
|
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
|
||||||
Acesse sua área pessoal e gerencie suas consultas
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-center">Entrar como Paciente</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form onSubmit={handleLogin} className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
placeholder="Digite seu email"
|
|
||||||
value={credentials.email}
|
|
||||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
|
||||||
required
|
|
||||||
className="mt-1"
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="password" className="block text-sm font-medium text-foreground">
|
|
||||||
Senha
|
|
||||||
</label>
|
|
||||||
<Input
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
placeholder="Digite sua senha"
|
|
||||||
value={credentials.password}
|
|
||||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
|
||||||
required
|
|
||||||
className="mt-1"
|
|
||||||
disabled={loading}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
className="w-full cursor-pointer"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? 'Entrando...' : 'Entrar na Minha Área'}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
|
||||||
<Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200">
|
|
||||||
<Link href="/">
|
|
||||||
Voltar ao Início
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,39 +1,99 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import { useState } from 'react'
|
|
||||||
import { useRouter } from 'next/navigation'
|
import { useEffect, useMemo, useState, type ChangeEvent, type FormEvent } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
import { useAuth } from '@/hooks/useAuth'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import { AuthenticationError } from '@/lib/auth'
|
import { AuthenticationError } from '@/lib/auth'
|
||||||
|
import { AUTH_STORAGE_KEYS } from '@/types/auth'
|
||||||
|
|
||||||
|
type UserRole = 'profissional' | 'paciente' | 'administrador'
|
||||||
|
|
||||||
|
const USER_ROLES: readonly UserRole[] = ['profissional', 'paciente', 'administrador'] as const
|
||||||
|
|
||||||
|
const roleConfig: Record<UserRole, {
|
||||||
|
title: string
|
||||||
|
subtitle: string
|
||||||
|
redirect: string
|
||||||
|
cta: string
|
||||||
|
}> = {
|
||||||
|
profissional: {
|
||||||
|
title: 'Login Profissional de Saúde',
|
||||||
|
subtitle: 'Entre com suas credenciais para acessar o sistema clínico',
|
||||||
|
redirect: '/profissional',
|
||||||
|
cta: 'Entrar como Profissional'
|
||||||
|
},
|
||||||
|
paciente: {
|
||||||
|
title: 'Sou Paciente',
|
||||||
|
subtitle: 'Acesse sua área pessoal e gerencie suas consultas',
|
||||||
|
redirect: '/paciente',
|
||||||
|
cta: 'Entrar na Minha Área'
|
||||||
|
},
|
||||||
|
administrador: {
|
||||||
|
title: 'Login Administrativo',
|
||||||
|
subtitle: 'Gerencie agendas, pacientes e finanças da clínica',
|
||||||
|
redirect: '/dashboard',
|
||||||
|
cta: 'Entrar no Painel Administrativo'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserRole = (value: string | null): value is UserRole => {
|
||||||
|
if (!value) return false
|
||||||
|
return USER_ROLES.includes(value as UserRole)
|
||||||
|
}
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
const [credentials, setCredentials] = useState({ email: '', password: '' })
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const router = useRouter()
|
const [selectedRole, setSelectedRole] = useState<UserRole>('profissional')
|
||||||
const { login } = useAuth()
|
const { login } = useAuth()
|
||||||
|
const router = useRouter()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
|
|
||||||
const handleLogin = async (e: React.FormEvent) => {
|
useEffect(() => {
|
||||||
e.preventDefault()
|
const roleParam = searchParams.get('role')
|
||||||
|
if (isUserRole(roleParam)) {
|
||||||
|
setSelectedRole(roleParam)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, roleParam)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const storedRole = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
|
||||||
|
if (isUserRole(storedRole)) {
|
||||||
|
setSelectedRole(storedRole)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchParams])
|
||||||
|
|
||||||
|
const { title, subtitle, redirect, cta } = useMemo(() => roleConfig[selectedRole], [selectedRole])
|
||||||
|
|
||||||
|
const handleSelectRole = (role: UserRole) => {
|
||||||
|
setSelectedRole(role)
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
localStorage.setItem(AUTH_STORAGE_KEYS.USER_TYPE, role)
|
||||||
|
}
|
||||||
|
router.replace(`/login?role=${role}`, { scroll: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLogin = async (event: FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Tentar fazer login usando o contexto com tipo profissional
|
const success = await login(credentials.email, credentials.password, selectedRole)
|
||||||
const success = await login(credentials.email, credentials.password, 'profissional')
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
console.log('[LOGIN-PROFISSIONAL] Login bem-sucedido, redirecionando...')
|
router.push(redirect)
|
||||||
|
|
||||||
// Redirecionamento direto - solução que funcionou
|
|
||||||
window.location.href = '/profissional'
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err: unknown) {
|
||||||
console.error('[LOGIN-PROFISSIONAL] Erro no login:', err)
|
|
||||||
|
|
||||||
if (err instanceof AuthenticationError) {
|
if (err instanceof AuthenticationError) {
|
||||||
setError(err.message)
|
setError(err.message)
|
||||||
} else {
|
} else {
|
||||||
@ -49,18 +109,36 @@ export default function LoginPage() {
|
|||||||
<div className="max-w-md w-full space-y-8">
|
<div className="max-w-md w-full space-y-8">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="mt-6 text-3xl font-extrabold text-foreground">
|
<h2 className="mt-6 text-3xl font-extrabold text-foreground">
|
||||||
Login Profissional de Saúde
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm text-muted-foreground">
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
Entre com suas credenciais para acessar o sistema
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-center">Acesso ao Sistema</CardTitle>
|
<CardTitle className="text-center">Escolha como deseja entrar</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-6">
|
||||||
|
{USER_ROLES.map((role) => {
|
||||||
|
const isActive = role === selectedRole
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={role}
|
||||||
|
type="button"
|
||||||
|
variant={isActive ? 'default' : 'outline'}
|
||||||
|
className={`w-full text-sm h-auto py-3 px-4 transition-all ${isActive ? 'shadow-md' : 'hover:bg-primary/5'}`}
|
||||||
|
onClick={() => handleSelectRole(role)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{roleConfig[role].title}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleLogin} className="space-y-6">
|
<form onSubmit={handleLogin} className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
<label htmlFor="email" className="block text-sm font-medium text-foreground">
|
||||||
@ -71,7 +149,9 @@ export default function LoginPage() {
|
|||||||
type="email"
|
type="email"
|
||||||
placeholder="Digite seu email"
|
placeholder="Digite seu email"
|
||||||
value={credentials.email}
|
value={credentials.email}
|
||||||
onChange={(e) => setCredentials({...credentials, email: e.target.value})}
|
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setCredentials({ ...credentials, email: event.target.value })
|
||||||
|
}
|
||||||
required
|
required
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@ -87,7 +167,9 @@ export default function LoginPage() {
|
|||||||
type="password"
|
type="password"
|
||||||
placeholder="Digite sua senha"
|
placeholder="Digite sua senha"
|
||||||
value={credentials.password}
|
value={credentials.password}
|
||||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||||
|
setCredentials({ ...credentials, password: event.target.value })
|
||||||
|
}
|
||||||
required
|
required
|
||||||
className="mt-1"
|
className="mt-1"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@ -100,17 +182,17 @@ export default function LoginPage() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button
|
<Button type="submit" className="w-full cursor-pointer" disabled={loading}>
|
||||||
type="submit"
|
{loading ? 'Entrando...' : cta}
|
||||||
className="w-full cursor-pointer"
|
|
||||||
disabled={loading}
|
|
||||||
>
|
|
||||||
{loading ? 'Entrando...' : 'Entrar'}
|
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-4 text-center">
|
<div className="mt-4 text-center">
|
||||||
<Button variant="outline" asChild className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200">
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
asChild
|
||||||
|
className="w-full hover:!bg-primary hover:!text-white hover:!border-primary transition-all duration-200"
|
||||||
|
>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
Voltar ao Início
|
Voltar ao Início
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -1,95 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import { useAuth } from '@/hooks/useAuth'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
||||||
import { User, LogOut, Home } from 'lucide-react'
|
|
||||||
import Link from 'next/link'
|
|
||||||
import ProtectedRoute from '@/components/ProtectedRoute'
|
|
||||||
|
|
||||||
export default function PacientePage() {
|
|
||||||
const { logout, user } = useAuth()
|
|
||||||
|
|
||||||
const handleLogout = async () => {
|
|
||||||
console.log('[PACIENTE] Iniciando logout...')
|
|
||||||
await logout()
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
<Link href="/">
|
|
||||||
<Home className="h-4 w-4" />
|
|
||||||
Voltar ao Início
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
</div>
|
|
||||||
</ProtectedRoute>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Header } from "@/components/header"
|
import { Header } from "@/components/layout/marketing/Header"
|
||||||
import { HeroSection } from "@/components/hero-section"
|
import { HeroSection } from "@/components/hero-section"
|
||||||
import { Footer } from "@/components/footer"
|
import { Footer } from "@/components/layout/marketing/Footer"
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { buscarPacientePorId } from "@/lib/api";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|||||||
@ -16,7 +16,6 @@ import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload,
|
|||||||
import {
|
import {
|
||||||
Paciente,
|
Paciente,
|
||||||
PacienteInput,
|
PacienteInput,
|
||||||
buscarCepAPI,
|
|
||||||
criarPaciente,
|
criarPaciente,
|
||||||
atualizarPaciente,
|
atualizarPaciente,
|
||||||
uploadFotoPaciente,
|
uploadFotoPaciente,
|
||||||
@ -25,10 +24,14 @@ import {
|
|||||||
listarAnexos,
|
listarAnexos,
|
||||||
removerAnexo,
|
removerAnexo,
|
||||||
buscarPacientePorId,
|
buscarPacientePorId,
|
||||||
} from "@/lib/api";
|
verificarCpfDuplicado,
|
||||||
|
} from "@/features/pacientes/api";
|
||||||
|
import {
|
||||||
|
criarUsuarioPaciente,
|
||||||
|
CreateUserWithPasswordResponse,
|
||||||
|
} from "@/features/autenticacao/api";
|
||||||
|
|
||||||
import { validarCPFLocal } from "@/lib/utils";
|
import { validarCPFLocal, buscarCepAPI } from "@/lib/utils";
|
||||||
import { verificarCpfDuplicado } from "@/lib/api";
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -233,9 +236,11 @@ export function PatientRegistrationForm({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Erro ao validar CPF", err);
|
// Log the validation error for debugging.
|
||||||
}
|
// Keeping the catch block intentionally minimal to avoid empty-block lint errors.
|
||||||
|
console.error("Erro ao validar CPF", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
@ -253,14 +258,20 @@ export function PatientRegistrationForm({
|
|||||||
if (form.photo && saved?.id) {
|
if (form.photo && saved?.id) {
|
||||||
try {
|
try {
|
||||||
await uploadFotoPaciente(saved.id, form.photo);
|
await uploadFotoPaciente(saved.id, form.photo);
|
||||||
} catch {}
|
} catch (uploadError) {
|
||||||
|
// Não bloquear a criação/edição do paciente se a foto falhar.
|
||||||
|
console.warn("Falha ao enviar foto do paciente:", uploadError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (form.anexos.length && saved?.id) {
|
if (form.anexos.length && saved?.id) {
|
||||||
for (const f of form.anexos) {
|
for (const f of form.anexos) {
|
||||||
try {
|
try {
|
||||||
await adicionarAnexo(saved.id, f);
|
await adicionarAnexo(saved.id, f);
|
||||||
} catch {}
|
} catch (anexoError) {
|
||||||
|
// Registrar erro de anexo e continuar com os próximos arquivos.
|
||||||
|
console.warn("Falha ao adicionar anexo:", anexoError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -7,7 +7,6 @@ export function HeroSection() {
|
|||||||
<section className="py-8 lg:py-12 bg-background">
|
<section className="py-8 lg:py-12 bg-background">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="grid lg:grid-cols-2 gap-8 items-center">
|
<div className="grid lg:grid-cols-2 gap-8 items-center">
|
||||||
{}
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="inline-block px-4 py-2 bg-accent/10 text-accent rounded-full text-sm font-medium">
|
<div className="inline-block px-4 py-2 bg-accent/10 text-accent rounded-full text-sm font-medium">
|
||||||
@ -23,27 +22,25 @@ export function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent"
|
className="bg-primary hover:bg-primary/90 text-primary-foreground cursor-pointer shadow-sm shadow-blue-500/10 border border-transparent dark:shadow-none"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Link href="/login-paciente">Portal do Paciente</Link>
|
<Link href="/login?role=paciente">Portal do Paciente</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="text-primary border-primary bg-transparent cursor-pointer 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"
|
className="text-primary bg-transparent cursor-pointer shadow-sm shadow-blue-500/10 border border-primary hover:bg-blue-50 dark:shadow-none dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
<Link href="/login?role=profissional">Sou Profissional de Saúde</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-accent/20 to-primary/20 p-6">
|
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-accent/20 to-primary/20 p-6">
|
||||||
<img
|
<img
|
||||||
@ -55,7 +52,6 @@ export function HeroSection() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="mt-10 grid md:grid-cols-3 gap-6">
|
<div className="mt-10 grid md:grid-cols-3 gap-6">
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-3">
|
||||||
<div className="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
|
<div className="flex-shrink-0 w-8 h-8 bg-primary/10 rounded-full flex items-center justify-center">
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Bell, ChevronDown } from "lucide-react"
|
import { Bell } from "lucide-react"
|
||||||
import { useAuth } from "@/hooks/useAuth"
|
import { useAuth } from "@/hooks/useAuth"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
import { useState, useEffect, useRef } from "react"
|
import { useState, useEffect, useRef } from "react"
|
||||||
import { SidebarTrigger } from "../ui/sidebar"
|
import { SidebarTrigger } from "@/components/ui/sidebar"
|
||||||
|
|
||||||
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
|
export function PagesHeader({ title = "", subtitle = "" }: { title?: string, subtitle?: string }) {
|
||||||
const { logout, user } = useAuth();
|
const { logout, user } = useAuth();
|
||||||
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { usePathname } from "next/navigation"
|
import { usePathname } from "next/navigation"
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import {
|
import {
|
||||||
Sidebar as ShadSidebar,
|
Sidebar as ShadSidebar,
|
||||||
SidebarHeader,
|
SidebarHeader,
|
||||||
@ -72,22 +71,23 @@ export function Sidebar() {
|
|||||||
<SidebarGroupContent>
|
<SidebarGroupContent>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
{navigation.map((item) => {
|
{navigation.map((item) => {
|
||||||
const isActive = pathname === item.href ||
|
const isActive =
|
||||||
(pathname.startsWith(item.href + "/") && item.href !== "/dashboard")
|
pathname === item.href ||
|
||||||
|
(pathname.startsWith(item.href + "/") && item.href !== "/dashboard")
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarMenuItem key={item.name}>
|
<SidebarMenuItem key={item.name}>
|
||||||
<SidebarMenuButton asChild isActive={isActive}>
|
<SidebarMenuButton asChild isActive={isActive}>
|
||||||
<Link href={item.href} className="flex items-center">
|
<Link href={item.href} className="flex items-center">
|
||||||
<item.icon className="mr-3 h-4 w-4 shrink-0" />
|
<item.icon className="mr-3 h-4 w-4 shrink-0" />
|
||||||
<span className="truncate group-data-[collapsible=icon]:hidden">
|
<span className="truncate group-data-[collapsible=icon]:hidden">
|
||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarGroupContent>
|
</SidebarGroupContent>
|
||||||
@ -12,10 +12,8 @@ export function Footer() {
|
|||||||
<footer className="bg-background border-t border-border">
|
<footer className="bg-background border-t border-border">
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||||
<div className="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
|
<div className="flex flex-col md:flex-row items-center justify-between space-y-4 md:space-y-0">
|
||||||
{}
|
|
||||||
<div className="text-muted-foreground text-sm">© 2025 MEDI Connect</div>
|
<div className="text-muted-foreground text-sm">© 2025 MEDI Connect</div>
|
||||||
|
|
||||||
{}
|
|
||||||
<nav className="flex items-center space-x-8">
|
<nav className="flex items-center space-x-8">
|
||||||
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
|
<a href="#" className="text-muted-foreground hover:text-primary transition-colors text-sm">
|
||||||
Termos
|
Termos
|
||||||
@ -28,7 +26,6 @@ export function Footer() {
|
|||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{}
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -5,7 +5,7 @@ import Link from "next/link";
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Menu, X } from "lucide-react";
|
import { Menu, X } from "lucide-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { SimpleThemeToggle } from "@/components/simple-theme-toggle";
|
import { SimpleThemeToggle } from "@/components/layout/SimpleThemeToggle";
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
@ -22,7 +22,6 @@ export function Header() {
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{}
|
|
||||||
<nav className="hidden md:flex items-center gap-10">
|
<nav className="hidden md:flex items-center gap-10">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href="/"
|
||||||
@ -42,31 +41,30 @@ export function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="hidden md:flex items-center space-x-3">
|
<div className="hidden md:flex items-center space-x-3">
|
||||||
<SimpleThemeToggle />
|
<SimpleThemeToggle />
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
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"
|
className="text-primary bg-transparent shadow-sm shadow-blue-500/10 border border-primary hover:bg-blue-50 dark:shadow-none dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
|
<Link href="/login?role=paciente">Sou Paciente</Link>
|
||||||
<Link href="/login-paciente">Sou Paciente</Link>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent">
|
<Button
|
||||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
className="bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm shadow-blue-500/10 border border-transparent dark:shadow-none"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/login?role=profissional">Sou Profissional de Saúde</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="text-primary bg-transparent shadow-sm shadow-blue-500/10 border border-primary hover:bg-blue-50 dark:shadow-none dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/login?role=administrador">Sou Administrador de uma Clínica</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Link href="/login-admin">
|
|
||||||
<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 cursor-pointer"
|
|
||||||
>
|
|
||||||
Sou Administrador de uma Clínica
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
|
||||||
<button
|
<button
|
||||||
className="md:hidden p-2"
|
className="md:hidden p-2"
|
||||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
@ -76,7 +74,6 @@ export function Header() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{}
|
|
||||||
{isMenuOpen && (
|
{isMenuOpen && (
|
||||||
<div className="md:hidden py-4 border-t border-border">
|
<div className="md:hidden py-4 border-t border-border">
|
||||||
<nav className="flex flex-col space-y-4">
|
<nav className="flex flex-col space-y-4">
|
||||||
@ -98,22 +95,24 @@ export function Header() {
|
|||||||
<SimpleThemeToggle />
|
<SimpleThemeToggle />
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
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"
|
className="text-primary bg-transparent shadow-sm shadow-blue-500/10 border border-primary hover:bg-blue-50 dark:shadow-none dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<Link href="/login-paciente">Sou Paciente</Link>
|
<Link href="/login?role=paciente">Sou Paciente</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Button className="bg-primary hover:bg-primary/90 text-primary-foreground w-full shadow-sm shadow-blue-500/10 border border-blue-200 dark:shadow-none dark:border-transparent">
|
<Button
|
||||||
<Link href="/login">Sou Profissional de Saúde</Link>
|
className="bg-primary hover:bg-primary/90 text-primary-foreground w-full shadow-sm shadow-blue-500/10 border border-transparent dark:shadow-none"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/login?role=profissional">Sou Profissional de Saúde</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="text-primary bg-transparent w-full shadow-sm shadow-blue-500/10 border border-primary hover:bg-blue-50 dark:shadow-none dark:hover:bg-primary dark:hover:text-primary-foreground"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/login?role=administrador">Sou Administrador de uma Clínica</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<Link href="/login-admin">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
className="text-primary border-primary bg-transparent w-full 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 cursor-pointer"
|
|
||||||
>
|
|
||||||
Sou Administrador de uma Clínica
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
179
src/features/autenticacao/api/index.ts
Normal file
179
src/features/autenticacao/api/index.ts
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import httpClient from "@/lib/http";
|
||||||
|
import { API_KEY } from "@/lib/config"; // Necessário para algumas chamadas de auth
|
||||||
|
import { AUTH_ENDPOINTS } from "@/lib/env-config"; // Endpoints de autenticação
|
||||||
|
|
||||||
|
// ===== TIPOS DE AUTENTICAÇÃO E PERFIL =====
|
||||||
|
export type Profile = {
|
||||||
|
id: string;
|
||||||
|
full_name: string;
|
||||||
|
email: string;
|
||||||
|
// Adicione outros campos do perfil aqui conforme necessário
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateUserWithPasswordResponse = {
|
||||||
|
user_id: string;
|
||||||
|
email: string;
|
||||||
|
password?: string; // A senha só é retornada na criação
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UserData = {
|
||||||
|
id: string;
|
||||||
|
email: string;
|
||||||
|
full_name?: string;
|
||||||
|
phone_mobile?: string;
|
||||||
|
// ... outros campos do usuário
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginResponse = {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
user: UserData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RefreshTokenResponse = {
|
||||||
|
access_token: string;
|
||||||
|
refresh_token: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== PERFIS DE USUÁRIOS =====
|
||||||
|
export async function listarPerfis(): Promise<Profile[]> {
|
||||||
|
// O endpoint original usava um mock.apidog.com. Vamos assumir que o endpoint real seria /rest/v1/profiles
|
||||||
|
// ou que o httpClient já está configurado para o Supabase.
|
||||||
|
const url = `/rest/v1/profiles`; // Usando caminho relativo para o httpClient
|
||||||
|
const res = await httpClient.get(url);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Erro ao listar perfis: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== CRIAÇÃO DE USUÁRIOS (PACIENTE/MÉDICO) =====
|
||||||
|
// Assumindo que o Supabase Auth tem um endpoint de signup ou que a criação é feita via API de admin
|
||||||
|
// Este é um placeholder. A implementação exata pode variar.
|
||||||
|
export async function criarUsuarioPaciente(input: {
|
||||||
|
email: string;
|
||||||
|
full_name: string;
|
||||||
|
phone_mobile?: string;
|
||||||
|
}): Promise<CreateUserWithPasswordResponse> {
|
||||||
|
// Endpoint de signup do Supabase Auth
|
||||||
|
const url = AUTH_ENDPOINTS.SIGNUP || `/auth/v1/signup`; // Usar AUTH_ENDPOINTS se definido, senão um padrão
|
||||||
|
const res = await httpClient.post(url, {
|
||||||
|
email: input.email,
|
||||||
|
password: Math.random().toString(36).slice(-8), // Gerar senha temporária
|
||||||
|
data: { full_name: input.full_name, phone_mobile: input.phone_mobile, role: 'paciente' },
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'apikey': API_KEY, // API Key é necessária para signup direto no Supabase Auth
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro ao criar usuário paciente: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return { user_id: data.user.id, email: data.user.email, password: data.password };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function criarUsuarioMedico(input: {
|
||||||
|
email: string;
|
||||||
|
full_name: string;
|
||||||
|
phone_mobile?: string;
|
||||||
|
}): Promise<CreateUserWithPasswordResponse> {
|
||||||
|
const url = AUTH_ENDPOINTS.SIGNUP || `/auth/v1/signup`;
|
||||||
|
const res = await httpClient.post(url, {
|
||||||
|
email: input.email,
|
||||||
|
password: Math.random().toString(36).slice(-8), // Gerar senha temporária
|
||||||
|
data: { full_name: input.full_name, phone_mobile: input.phone_mobile, role: 'medico' },
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'apikey': API_KEY,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro ao criar usuário médico: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return { user_id: data.user.id, email: data.user.email, password: data.password };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== FUNÇÕES DE AUTENTICAÇÃO PRINCIPAIS (MOVIDAS DE auth.ts) =====
|
||||||
|
// Estas funções serão movidas de src/lib/auth.ts para cá
|
||||||
|
|
||||||
|
export async function loginUser(
|
||||||
|
email: string,
|
||||||
|
password: string,
|
||||||
|
userType: 'profissional' | 'paciente' | 'administrador'
|
||||||
|
): Promise<LoginResponse> {
|
||||||
|
const url = AUTH_ENDPOINTS.LOGIN;
|
||||||
|
const payload = { email, password };
|
||||||
|
|
||||||
|
const res = await httpClient.post(url, payload, {
|
||||||
|
headers: {
|
||||||
|
'apikey': API_KEY,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro no login: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return { access_token: data.access_token, refresh_token: data.refresh_token, user: data.user };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function logoutUser(token: string): Promise<void> {
|
||||||
|
const url = AUTH_ENDPOINTS.LOGOUT;
|
||||||
|
const res = await httpClient.post(url, null, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'apikey': API_KEY,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro no logout: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function refreshAuthToken(refreshToken: string): Promise<RefreshTokenResponse> {
|
||||||
|
const url = AUTH_ENDPOINTS.REFRESH;
|
||||||
|
const res = await httpClient.post(url, { refresh_token: refreshToken }, {
|
||||||
|
headers: {
|
||||||
|
'apikey': API_KEY,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro ao renovar token: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return { access_token: data.access_token, refresh_token: data.refresh_token };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCurrentUser(token: string): Promise<UserData> {
|
||||||
|
const url = AUTH_ENDPOINTS.USER;
|
||||||
|
const res = await httpClient.get(url, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'apikey': API_KEY,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro ao obter usuário atual: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
return data.user; // Assumindo que a resposta tem um objeto 'user'
|
||||||
|
}
|
||||||
@ -2,19 +2,7 @@
|
|||||||
import httpClient from "@/lib/http";
|
import httpClient from "@/lib/http";
|
||||||
import type { Paciente, PacienteInput } from "@/features/pacientes/types";
|
import type { Paciente, PacienteInput } from "@/features/pacientes/types";
|
||||||
|
|
||||||
// TODO: Essas dependências foram movidas de lib/api.ts e podem precisar de ajustes.
|
// A constante `REST` foi removida para usar a baseURL do httpClient.
|
||||||
const API_BASE =
|
|
||||||
process.env.NEXT_PUBLIC_API_BASE ?? "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const REST = `${API_BASE}/rest/v1`;
|
|
||||||
|
|
||||||
// Funções auxiliares que estavam no escopo de api.ts
|
|
||||||
// Elas provavelmente deveriam estar em um arquivo de utilitários compartilhado.
|
|
||||||
declare function baseHeaders(): Record<string, string>;
|
|
||||||
declare function withPrefer(
|
|
||||||
headers: Record<string, string>,
|
|
||||||
prefer: string
|
|
||||||
): Record<string, string>;
|
|
||||||
declare function parse<T>(res: Response): Promise<T>;
|
|
||||||
|
|
||||||
function rangeHeaders(page?: number, limit?: number): Record<string, string> {
|
function rangeHeaders(page?: number, limit?: number): Record<string, string> {
|
||||||
if (!page || !limit) return {};
|
if (!page || !limit) return {};
|
||||||
@ -32,7 +20,8 @@ export async function listarPacientes(params?: {
|
|||||||
const qs = new URLSearchParams();
|
const qs = new URLSearchParams();
|
||||||
if (params?.q) qs.set("q", params.q);
|
if (params?.q) qs.set("q", params.q);
|
||||||
|
|
||||||
const url = `${REST}/patients${qs.toString() ? `?${qs.toString()}` : ""}`;
|
// Utiliza caminho relativo, o httpClient adicionará a base.
|
||||||
|
const url = `/rest/v1/patients${qs.toString() ? `?${qs.toString()}` : ""}`;
|
||||||
const res = await httpClient.get(url, {
|
const res = await httpClient.get(url, {
|
||||||
headers: {
|
headers: {
|
||||||
...rangeHeaders(params?.page, params?.limit),
|
...rangeHeaders(params?.page, params?.limit),
|
||||||
@ -43,10 +32,9 @@ export async function listarPacientes(params?: {
|
|||||||
throw new Error(`Erro ao listar pacientes: ${res.statusText}`);
|
throw new Error(`Erro ao listar pacientes: ${res.statusText}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nova função para busca avançada de pacientes
|
|
||||||
export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
||||||
if (!termo || termo.trim().length < 2) {
|
if (!termo || termo.trim().length < 2) {
|
||||||
return [];
|
return [];
|
||||||
@ -55,28 +43,19 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
|||||||
const searchTerm = termo.toLowerCase().trim();
|
const searchTerm = termo.toLowerCase().trim();
|
||||||
const digitsOnly = searchTerm.replace(/\D/g, "");
|
const digitsOnly = searchTerm.replace(/\D/g, "");
|
||||||
|
|
||||||
// Monta queries para buscar em múltiplos campos
|
|
||||||
const queries = [];
|
const queries = [];
|
||||||
|
|
||||||
// Busca por ID se parece com UUID
|
|
||||||
if (searchTerm.includes("-") && searchTerm.length > 10) {
|
if (searchTerm.includes("-") && searchTerm.length > 10) {
|
||||||
queries.push(`id=eq.${searchTerm}`);
|
queries.push(`id=eq.${searchTerm}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca por CPF (com e sem formatação)
|
|
||||||
if (digitsOnly.length >= 11) {
|
if (digitsOnly.length >= 11) {
|
||||||
queries.push(`cpf=eq.${digitsOnly}`);
|
queries.push(`cpf=eq.${digitsOnly}`);
|
||||||
} else if (digitsOnly.length >= 3) {
|
} else if (digitsOnly.length >= 3) {
|
||||||
queries.push(`cpf=ilike.*${digitsOnly}*`);
|
queries.push(`cpf=ilike.*${digitsOnly}*`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca por nome (usando ilike para busca case-insensitive)
|
|
||||||
if (searchTerm.length >= 2) {
|
if (searchTerm.length >= 2) {
|
||||||
queries.push(`full_name=ilike.*${searchTerm}*`);
|
queries.push(`full_name=ilike.*${searchTerm}*`);
|
||||||
queries.push(`social_name=ilike.*${searchTerm}*`);
|
queries.push(`social_name=ilike.*${searchTerm}*`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Busca por email se contém @
|
|
||||||
if (searchTerm.includes("@")) {
|
if (searchTerm.includes("@")) {
|
||||||
queries.push(`email=ilike.*${searchTerm}*`);
|
queries.push(`email=ilike.*${searchTerm}*`);
|
||||||
}
|
}
|
||||||
@ -84,12 +63,11 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
|||||||
const results: Paciente[] = [];
|
const results: Paciente[] = [];
|
||||||
const seenIds = new Set<string>();
|
const seenIds = new Set<string>();
|
||||||
|
|
||||||
// Executa as buscas e combina resultados únicos
|
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
try {
|
try {
|
||||||
const url = `${REST}/patients?${query}&limit=10`;
|
const url = `/rest/v1/patients?${query}&limit=10`;
|
||||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
const res = await httpClient.get(url);
|
||||||
const arr = await parse<Paciente[]>(res);
|
const arr: Paciente[] = await res.json();
|
||||||
|
|
||||||
if (arr?.length > 0) {
|
if (arr?.length > 0) {
|
||||||
for (const paciente of arr) {
|
for (const paciente of arr) {
|
||||||
@ -104,63 +82,98 @@ export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results.slice(0, 20); // Limita a 20 resultados
|
return results.slice(0, 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function buscarPacientePorId(id: string | number): Promise<Paciente> {
|
export async function buscarPacientePorId(id: string | number): Promise<Paciente> {
|
||||||
const url = `${REST}/patients?id=eq.${id}`;
|
const url = `/rest/v1/patients?id=eq.${id}&limit=1`;
|
||||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
const res = await httpClient.get(url);
|
||||||
const arr = await parse<Paciente[]>(res);
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("404: Paciente não encontrado");
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr: Paciente[] = await res.json();
|
||||||
if (!arr?.length) throw new Error("404: Paciente não encontrado");
|
if (!arr?.length) throw new Error("404: Paciente não encontrado");
|
||||||
return arr[0];
|
return arr[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function criarPaciente(input: PacienteInput): Promise<Paciente> {
|
export async function criarPaciente(input: PacienteInput): Promise<Paciente> {
|
||||||
const url = `${REST}/patients`;
|
const url = `/rest/v1/patients`;
|
||||||
const res = await fetch(url, {
|
const res = await httpClient.post(url, input, {
|
||||||
method: "POST",
|
headers: {
|
||||||
headers: withPrefer(
|
Prefer: "return=representation",
|
||||||
{ ...baseHeaders(), "Content-Type": "application/json" },
|
},
|
||||||
"return=representation"
|
|
||||||
),
|
|
||||||
body: JSON.stringify(input),
|
|
||||||
});
|
});
|
||||||
const arr = await parse<Paciente[] | Paciente>(res);
|
|
||||||
return Array.isArray(arr) ? arr[0] : (arr as Paciente);
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro ao criar paciente: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr: Paciente[] = await res.json();
|
||||||
|
return arr[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function atualizarPaciente(
|
export async function atualizarPaciente(
|
||||||
id: string | number,
|
id: string | number,
|
||||||
input: PacienteInput
|
input: Partial<PacienteInput>
|
||||||
): Promise<Paciente> {
|
): Promise<Paciente> {
|
||||||
const url = `${REST}/patients?id=eq.${id}`;
|
const url = `/rest/v1/patients?id=eq.${id}`;
|
||||||
const res = await fetch(url, {
|
const res = await httpClient.patch(url, input, {
|
||||||
method: "PATCH",
|
headers: {
|
||||||
headers: withPrefer(
|
Prefer: "return=representation",
|
||||||
{ ...baseHeaders(), "Content-Type": "application/json" },
|
},
|
||||||
"return=representation"
|
|
||||||
),
|
|
||||||
body: JSON.stringify(input),
|
|
||||||
});
|
});
|
||||||
const arr = await parse<Paciente[] | Paciente>(res);
|
|
||||||
return Array.isArray(arr) ? arr[0] : (arr as Paciente);
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro ao atualizar paciente: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr: Paciente[] = await res.json();
|
||||||
|
return arr[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function excluirPaciente(id: string | number): Promise<void> {
|
export async function excluirPaciente(id: string | number): Promise<void> {
|
||||||
const url = `${REST}/patients?id=eq.${id}`;
|
const url = `/rest/v1/patients?id=eq.${id}`;
|
||||||
const res = await fetch(url, { method: "DELETE", headers: baseHeaders() });
|
const res = await httpClient.delete(url);
|
||||||
await parse<any>(res);
|
|
||||||
|
if (!res.ok && res.status !== 204) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro ao excluir paciente: ${error.message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// ===== PACIENTES (Extra: verificação de CPF duplicado) =====
|
|
||||||
export async function verificarCpfDuplicado(cpf: string): Promise<boolean> {
|
export async function verificarCpfDuplicado(cpf: string): Promise<boolean> {
|
||||||
const clean = (cpf || "").replace(/\D/g, "");
|
const clean = (cpf || "").replace(/\D/g, "");
|
||||||
const url = `${API_BASE}/rest/v1/patients?cpf=eq.${clean}&select=id`;
|
if (!clean) return false;
|
||||||
|
|
||||||
const res = await fetch(url, {
|
const url = `/rest/v1/patients?cpf=eq.${clean}&select=id`;
|
||||||
method: "GET",
|
const res = await httpClient.get(url);
|
||||||
headers: baseHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json().catch(() => []);
|
if (!res.ok) {
|
||||||
|
console.error("Erro ao verificar CPF:", res.statusText);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: any[] = await res.json().catch(() => []);
|
||||||
return Array.isArray(data) && data.length > 0;
|
return Array.isArray(data) && data.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Funções "stub" para pacientes
|
||||||
|
export const uploadFotoPacienteAPI = async (
|
||||||
|
pacienteId: string,
|
||||||
|
foto: File
|
||||||
|
) => {
|
||||||
|
console.log(`Upload da foto do paciente ${pacienteId}:`, foto.name);
|
||||||
|
return { success: true, message: "Foto do paciente enviada com sucesso." };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadAnexoPacienteAPI = async (
|
||||||
|
pacienteId: string,
|
||||||
|
anexo: File
|
||||||
|
) => {
|
||||||
|
console.log(`Upload do anexo do paciente ${pacienteId}:`, anexo.name);
|
||||||
|
return { success: true, message: "Anexo do paciente enviado com sucesso." };
|
||||||
|
};
|
||||||
|
|||||||
168
src/features/profissionais/api/index.ts
Normal file
168
src/features/profissionais/api/index.ts
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
// src/features/profissionais/api/index.ts
|
||||||
|
import httpClient from "@/lib/http";
|
||||||
|
import type { Medico, MedicoInput } from "@/features/profissionais/types";
|
||||||
|
|
||||||
|
function rangeHeaders(page?: number, limit?: number): Record<string, string> {
|
||||||
|
if (!page || !limit) return {};
|
||||||
|
const start = (page - 1) * limit;
|
||||||
|
const end = start + limit - 1;
|
||||||
|
return { Range: `${start}-${end}`, "Range-Unit": "items" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== MÉDICOS (CRUD) =====
|
||||||
|
export async function listarMedicos(params?: {
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
q?: string;
|
||||||
|
}): Promise<Medico[]> {
|
||||||
|
const qs = new URLSearchParams();
|
||||||
|
if (params?.q) qs.set("q", params.q);
|
||||||
|
|
||||||
|
const url = `/rest/v1/doctors${qs.toString() ? `?${qs.toString()}` : ""}`;
|
||||||
|
const res = await httpClient.get(url, {
|
||||||
|
headers: {
|
||||||
|
...rangeHeaders(params?.page, params?.limit),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`Erro ao listar médicos: ${res.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buscarMedicos(termo: string): Promise<Medico[]> {
|
||||||
|
if (!termo || termo.trim().length < 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTerm = termo.toLowerCase().trim();
|
||||||
|
const digitsOnly = searchTerm.replace(/\D/g, '');
|
||||||
|
|
||||||
|
const queries = [];
|
||||||
|
|
||||||
|
if (searchTerm.includes('-') && searchTerm.length > 10) {
|
||||||
|
queries.push(`id=eq.${searchTerm}`);
|
||||||
|
}
|
||||||
|
if (digitsOnly.length >= 3) {
|
||||||
|
queries.push(`crm=ilike.*${digitsOnly}*`);
|
||||||
|
}
|
||||||
|
if (searchTerm.length >= 2) {
|
||||||
|
queries.push(`full_name=ilike.*${searchTerm}*`);
|
||||||
|
queries.push(`nome_social=ilike.*${searchTerm}*`);
|
||||||
|
}
|
||||||
|
if (searchTerm.includes('@')) {
|
||||||
|
queries.push(`email=ilike.*${searchTerm}*`);
|
||||||
|
}
|
||||||
|
if (searchTerm.length >= 2) {
|
||||||
|
queries.push(`specialty=ilike.*${searchTerm}*`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: Medico[] = [];
|
||||||
|
const seenIds = new Set<string>();
|
||||||
|
|
||||||
|
for (const query of queries) {
|
||||||
|
try {
|
||||||
|
const url = `/rest/v1/doctors?${query}&limit=10`;
|
||||||
|
const res = await httpClient.get(url);
|
||||||
|
const arr: Medico[] = await res.json();
|
||||||
|
|
||||||
|
if (arr?.length > 0) {
|
||||||
|
for (const medico of arr) {
|
||||||
|
if (!seenIds.has(medico.id)) {
|
||||||
|
seenIds.add(medico.id);
|
||||||
|
results.push(medico);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Erro na busca com query: ${query}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.slice(0, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function buscarMedicoPorId(id: string | number): Promise<Medico> {
|
||||||
|
const url = `/rest/v1/doctors?id=eq.${id}&limit=1`;
|
||||||
|
const res = await httpClient.get(url);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("404: Médico não encontrado");
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr: Medico[] = await res.json();
|
||||||
|
if (!arr?.length) throw new Error("404: Médico não encontrado");
|
||||||
|
return arr[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function criarMedico(input: MedicoInput): Promise<Medico> {
|
||||||
|
const url = `/rest/v1/doctors`;
|
||||||
|
const res = await httpClient.post(url, input, {
|
||||||
|
headers: {
|
||||||
|
Prefer: "return=representation",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro ao criar médico: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr: Medico[] = await res.json();
|
||||||
|
return arr[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function atualizarMedico(
|
||||||
|
id: string | number,
|
||||||
|
input: Partial<MedicoInput>
|
||||||
|
): Promise<Medico> {
|
||||||
|
const url = `/rest/v1/doctors?id=eq.${id}`;
|
||||||
|
const res = await httpClient.patch(url, input, {
|
||||||
|
headers: {
|
||||||
|
Prefer: "return=representation",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro ao atualizar médico: ${error.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const arr: Medico[] = await res.json();
|
||||||
|
return arr[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function excluirMedico(id: string | number): Promise<void> {
|
||||||
|
const url = `/rest/v1/doctors?id=eq.${id}`;
|
||||||
|
const res = await httpClient.delete(url);
|
||||||
|
|
||||||
|
if (!res.ok && res.status !== 204) {
|
||||||
|
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||||
|
throw new Error(`Erro ao excluir médico: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funções "stub" para profissionais
|
||||||
|
export const uploadFotoProfissionalAPI = async (
|
||||||
|
profissionalId: string,
|
||||||
|
foto: File
|
||||||
|
) => {
|
||||||
|
console.log(`Upload da foto do profissional ${profissionalId}:`, foto.name);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Foto do profissional enviada com sucesso.",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadAnexoProfissionalAPI = async (
|
||||||
|
profissionalId: string,
|
||||||
|
anexo: File
|
||||||
|
) => {
|
||||||
|
console.log(`Upload do anexo do profissional ${profissionalId}:`, anexo.name);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Anexo do profissional enviado com sucesso.",
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -1,6 +1,5 @@
|
|||||||
// src/features/profissionais/types/index.ts
|
// src/features/profissionais/types/index.ts
|
||||||
|
|
||||||
// ===== MÉDICOS =====
|
|
||||||
export type FormacaoAcademica = {
|
export type FormacaoAcademica = {
|
||||||
instituicao: string;
|
instituicao: string;
|
||||||
curso: string;
|
curso: string;
|
||||||
@ -14,10 +13,9 @@ export type DadosBancarios = {
|
|||||||
tipo_conta: string;
|
tipo_conta: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ===== MÉDICOS =====
|
|
||||||
export type Medico = {
|
export type Medico = {
|
||||||
id: string;
|
id: string;
|
||||||
full_name: string; // Altere 'nome' para 'full_name'
|
full_name: string;
|
||||||
nome_social?: string | null;
|
nome_social?: string | null;
|
||||||
cpf?: string;
|
cpf?: string;
|
||||||
rg?: string | null;
|
rg?: string | null;
|
||||||
@ -55,9 +53,6 @@ export type Medico = {
|
|||||||
user_id?: string;
|
user_id?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// ===== MÉDICOS =====
|
|
||||||
// ...existing code...
|
|
||||||
export type MedicoInput = {
|
export type MedicoInput = {
|
||||||
user_id?: string | null;
|
user_id?: string | null;
|
||||||
crm: string;
|
crm: string;
|
||||||
|
|||||||
590
src/lib/api.ts
590
src/lib/api.ts
@ -1,590 +0,0 @@
|
|||||||
// lib/api.ts
|
|
||||||
|
|
||||||
export type ApiOk<T = any> = {
|
|
||||||
success?: boolean;
|
|
||||||
data: T;
|
|
||||||
message?: string;
|
|
||||||
pagination?: {
|
|
||||||
current_page?: number;
|
|
||||||
per_page?: number;
|
|
||||||
total_pages?: number;
|
|
||||||
total?: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== TIPOS COMUNS =====
|
|
||||||
export type Endereco = {
|
|
||||||
cep?: string;
|
|
||||||
logradouro?: string;
|
|
||||||
numero?: string;
|
|
||||||
complemento?: string;
|
|
||||||
bairro?: string;
|
|
||||||
cidade?: string;
|
|
||||||
estado?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== PACIENTES =====
|
|
||||||
export type Paciente = {
|
|
||||||
id: string;
|
|
||||||
full_name: string;
|
|
||||||
social_name?: string | null;
|
|
||||||
cpf?: string;
|
|
||||||
rg?: string | null;
|
|
||||||
sex?: string | null;
|
|
||||||
birth_date?: string | null;
|
|
||||||
phone_mobile?: string;
|
|
||||||
email?: string;
|
|
||||||
cep?: string | null;
|
|
||||||
street?: string | null;
|
|
||||||
number?: string | null;
|
|
||||||
complement?: string | null;
|
|
||||||
neighborhood?: string | null;
|
|
||||||
city?: string | null;
|
|
||||||
state?: string | null;
|
|
||||||
notes?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PacienteInput = {
|
|
||||||
full_name: string;
|
|
||||||
social_name?: string | null;
|
|
||||||
cpf: string;
|
|
||||||
rg?: string | null;
|
|
||||||
sex?: string | null;
|
|
||||||
birth_date?: string | null;
|
|
||||||
phone_mobile?: string | null;
|
|
||||||
email?: string | null;
|
|
||||||
cep?: string | null;
|
|
||||||
street?: string | null;
|
|
||||||
number?: string | null;
|
|
||||||
complement?: string | null;
|
|
||||||
neighborhood?: string | null;
|
|
||||||
city?: string | null;
|
|
||||||
state?: string | null;
|
|
||||||
notes?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// ===== MÉDICOS =====
|
|
||||||
export type FormacaoAcademica = {
|
|
||||||
instituicao: string;
|
|
||||||
curso: string;
|
|
||||||
ano_conclusao: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DadosBancarios = {
|
|
||||||
banco: string;
|
|
||||||
agencia: string;
|
|
||||||
conta: string;
|
|
||||||
tipo_conta: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// ===== MÉDICOS =====
|
|
||||||
export type Medico = {
|
|
||||||
id: string;
|
|
||||||
full_name: string; // Altere 'nome' para 'full_name'
|
|
||||||
nome_social?: string | null;
|
|
||||||
cpf?: string;
|
|
||||||
rg?: string | null;
|
|
||||||
sexo?: string | null;
|
|
||||||
data_nascimento?: string | null;
|
|
||||||
telefone?: string;
|
|
||||||
celular?: string;
|
|
||||||
contato_emergencia?: string;
|
|
||||||
email?: string;
|
|
||||||
crm?: string;
|
|
||||||
estado_crm?: string;
|
|
||||||
rqe?: string;
|
|
||||||
formacao_academica?: FormacaoAcademica[];
|
|
||||||
curriculo_url?: string | null;
|
|
||||||
especialidade?: string;
|
|
||||||
observacoes?: string | null;
|
|
||||||
foto_url?: string | null;
|
|
||||||
tipo_vinculo?: string;
|
|
||||||
dados_bancarios?: DadosBancarios;
|
|
||||||
agenda_horario?: string;
|
|
||||||
valor_consulta?: number | string;
|
|
||||||
active?: boolean;
|
|
||||||
cep?: string;
|
|
||||||
city?: string;
|
|
||||||
complement?: string;
|
|
||||||
neighborhood?: string;
|
|
||||||
number?: string;
|
|
||||||
phone2?: string;
|
|
||||||
state?: string;
|
|
||||||
street?: string;
|
|
||||||
created_at?: string;
|
|
||||||
created_by?: string;
|
|
||||||
updated_at?: string;
|
|
||||||
updated_by?: string;
|
|
||||||
user_id?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// ===== MÉDICOS =====
|
|
||||||
// ...existing code...
|
|
||||||
export type MedicoInput = {
|
|
||||||
user_id?: string | null;
|
|
||||||
crm: string;
|
|
||||||
crm_uf: string;
|
|
||||||
specialty: string;
|
|
||||||
full_name: string;
|
|
||||||
cpf: string;
|
|
||||||
email: string;
|
|
||||||
phone_mobile: string;
|
|
||||||
phone2?: string | null;
|
|
||||||
cep: string;
|
|
||||||
street: string;
|
|
||||||
number: string;
|
|
||||||
complement?: string;
|
|
||||||
neighborhood?: string;
|
|
||||||
city: string;
|
|
||||||
state: string;
|
|
||||||
birth_date: string | null;
|
|
||||||
rg?: string | null;
|
|
||||||
active?: boolean;
|
|
||||||
created_by?: string | null;
|
|
||||||
updated_by?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ===== CONFIG =====
|
|
||||||
const API_BASE =
|
|
||||||
process.env.NEXT_PUBLIC_API_BASE ?? "https://yuanqfswhberkoevtmfr.supabase.co";
|
|
||||||
const REST = `${API_BASE}/rest/v1`;
|
|
||||||
|
|
||||||
// Token salvo no browser (aceita auth_token ou token)
|
|
||||||
function getAuthToken(): string | null {
|
|
||||||
if (typeof window === "undefined") return null;
|
|
||||||
return (
|
|
||||||
localStorage.getItem("auth_token") ||
|
|
||||||
localStorage.getItem("token") ||
|
|
||||||
sessionStorage.getItem("auth_token") ||
|
|
||||||
sessionStorage.getItem("token")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cabeçalhos base
|
|
||||||
function baseHeaders(): Record<string, string> {
|
|
||||||
const h: Record<string, string> = {
|
|
||||||
apikey:
|
|
||||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ",
|
|
||||||
Accept: "application/json",
|
|
||||||
};
|
|
||||||
const jwt = getAuthToken();
|
|
||||||
if (jwt) h.Authorization = `Bearer ${jwt}`;
|
|
||||||
return h;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Para POST/PATCH/DELETE e para GET com count
|
|
||||||
function withPrefer(h: Record<string, string>, prefer: string) {
|
|
||||||
return { ...h, Prefer: prefer };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse genérico
|
|
||||||
async function parse<T>(res: Response): Promise<T> {
|
|
||||||
let json: any = null;
|
|
||||||
try {
|
|
||||||
json = await res.json();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Erro ao parsear a resposta:", err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (json?.data ?? json) as T;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Helper de paginação (Range/Range-Unit)
|
|
||||||
function rangeHeaders(page?: number, limit?: number): Record<string, string> {
|
|
||||||
if (!page || !limit) return {};
|
|
||||||
const start = (page - 1) * limit;
|
|
||||||
const end = start + limit - 1;
|
|
||||||
return { Range: `${start}-${end}`, "Range-Unit": "items" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== PACIENTES (CRUD) =====
|
|
||||||
export async function listarPacientes(params?: {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
q?: string;
|
|
||||||
}): Promise<Paciente[]> {
|
|
||||||
const qs = new URLSearchParams();
|
|
||||||
if (params?.q) qs.set("q", params.q);
|
|
||||||
|
|
||||||
const url = `${REST}/patients${qs.toString() ? `?${qs.toString()}` : ""}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
...baseHeaders(),
|
|
||||||
...rangeHeaders(params?.page, params?.limit),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return await parse<Paciente[]>(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Nova função para busca avançada de pacientes
|
|
||||||
export async function buscarPacientes(termo: string): Promise<Paciente[]> {
|
|
||||||
if (!termo || termo.trim().length < 2) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchTerm = termo.toLowerCase().trim();
|
|
||||||
const digitsOnly = searchTerm.replace(/\D/g, '');
|
|
||||||
|
|
||||||
// Monta queries para buscar em múltiplos campos
|
|
||||||
const queries = [];
|
|
||||||
|
|
||||||
// Busca por ID se parece com UUID
|
|
||||||
if (searchTerm.includes('-') && searchTerm.length > 10) {
|
|
||||||
queries.push(`id=eq.${searchTerm}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por CPF (com e sem formatação)
|
|
||||||
if (digitsOnly.length >= 11) {
|
|
||||||
queries.push(`cpf=eq.${digitsOnly}`);
|
|
||||||
} else if (digitsOnly.length >= 3) {
|
|
||||||
queries.push(`cpf=ilike.*${digitsOnly}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por nome (usando ilike para busca case-insensitive)
|
|
||||||
if (searchTerm.length >= 2) {
|
|
||||||
queries.push(`full_name=ilike.*${searchTerm}*`);
|
|
||||||
queries.push(`social_name=ilike.*${searchTerm}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por email se contém @
|
|
||||||
if (searchTerm.includes('@')) {
|
|
||||||
queries.push(`email=ilike.*${searchTerm}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: Paciente[] = [];
|
|
||||||
const seenIds = new Set<string>();
|
|
||||||
|
|
||||||
// Executa as buscas e combina resultados únicos
|
|
||||||
for (const query of queries) {
|
|
||||||
try {
|
|
||||||
const url = `${REST}/patients?${query}&limit=10`;
|
|
||||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
|
||||||
const arr = await parse<Paciente[]>(res);
|
|
||||||
|
|
||||||
if (arr?.length > 0) {
|
|
||||||
for (const paciente of arr) {
|
|
||||||
if (!seenIds.has(paciente.id)) {
|
|
||||||
seenIds.add(paciente.id);
|
|
||||||
results.push(paciente);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Erro na busca com query: ${query}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.slice(0, 20); // Limita a 20 resultados
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function buscarPacientePorId(id: string | number): Promise<Paciente> {
|
|
||||||
const url = `${REST}/patients?id=eq.${id}`;
|
|
||||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
|
||||||
const arr = await parse<Paciente[]>(res);
|
|
||||||
if (!arr?.length) throw new Error("404: Paciente não encontrado");
|
|
||||||
return arr[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function criarPaciente(input: PacienteInput): Promise<Paciente> {
|
|
||||||
const url = `${REST}/patients`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
|
|
||||||
body: JSON.stringify(input),
|
|
||||||
});
|
|
||||||
const arr = await parse<Paciente[] | Paciente>(res);
|
|
||||||
return Array.isArray(arr) ? arr[0] : (arr as Paciente);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function atualizarPaciente(id: string | number, input: PacienteInput): Promise<Paciente> {
|
|
||||||
const url = `${REST}/patients?id=eq.${id}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
|
|
||||||
body: JSON.stringify(input),
|
|
||||||
});
|
|
||||||
const arr = await parse<Paciente[] | Paciente>(res);
|
|
||||||
return Array.isArray(arr) ? arr[0] : (arr as Paciente);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function excluirPaciente(id: string | number): Promise<void> {
|
|
||||||
const url = `${REST}/patients?id=eq.${id}`;
|
|
||||||
const res = await fetch(url, { method: "DELETE", headers: baseHeaders() });
|
|
||||||
await parse<any>(res);
|
|
||||||
}
|
|
||||||
// ===== PACIENTES (Extra: verificação de CPF duplicado) =====
|
|
||||||
export async function verificarCpfDuplicado(cpf: string): Promise<boolean> {
|
|
||||||
const clean = (cpf || "").replace(/\D/g, "");
|
|
||||||
const url = `${API_BASE}/rest/v1/patients?cpf=eq.${clean}&select=id`;
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: baseHeaders(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await res.json().catch(() => []);
|
|
||||||
return Array.isArray(data) && data.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ===== MÉDICOS (CRUD) =====
|
|
||||||
export async function listarMedicos(params?: {
|
|
||||||
page?: number;
|
|
||||||
limit?: number;
|
|
||||||
q?: string;
|
|
||||||
}): Promise<Medico[]> {
|
|
||||||
const qs = new URLSearchParams();
|
|
||||||
if (params?.q) qs.set("q", params.q);
|
|
||||||
|
|
||||||
const url = `${REST}/doctors${qs.toString() ? `?${qs.toString()}` : ""}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
...baseHeaders(),
|
|
||||||
...rangeHeaders(params?.page, params?.limit),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return await parse<Medico[]>(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nova função para busca avançada de médicos
|
|
||||||
export async function buscarMedicos(termo: string): Promise<Medico[]> {
|
|
||||||
if (!termo || termo.trim().length < 2) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const searchTerm = termo.toLowerCase().trim();
|
|
||||||
const digitsOnly = searchTerm.replace(/\D/g, '');
|
|
||||||
|
|
||||||
// Monta queries para buscar em múltiplos campos
|
|
||||||
const queries = [];
|
|
||||||
|
|
||||||
// Busca por ID se parece com UUID
|
|
||||||
if (searchTerm.includes('-') && searchTerm.length > 10) {
|
|
||||||
queries.push(`id=eq.${searchTerm}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por CRM (com e sem formatação)
|
|
||||||
if (digitsOnly.length >= 3) {
|
|
||||||
queries.push(`crm=ilike.*${digitsOnly}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por nome (usando ilike para busca case-insensitive)
|
|
||||||
if (searchTerm.length >= 2) {
|
|
||||||
queries.push(`full_name=ilike.*${searchTerm}*`);
|
|
||||||
queries.push(`nome_social=ilike.*${searchTerm}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por email se contém @
|
|
||||||
if (searchTerm.includes('@')) {
|
|
||||||
queries.push(`email=ilike.*${searchTerm}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Busca por especialidade
|
|
||||||
if (searchTerm.length >= 2) {
|
|
||||||
queries.push(`specialty=ilike.*${searchTerm}*`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const results: Medico[] = [];
|
|
||||||
const seenIds = new Set<string>();
|
|
||||||
|
|
||||||
// Executa as buscas e combina resultados únicos
|
|
||||||
for (const query of queries) {
|
|
||||||
try {
|
|
||||||
const url = `${REST}/doctors?${query}&limit=10`;
|
|
||||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
|
||||||
const arr = await parse<Medico[]>(res);
|
|
||||||
|
|
||||||
if (arr?.length > 0) {
|
|
||||||
for (const medico of arr) {
|
|
||||||
if (!seenIds.has(medico.id)) {
|
|
||||||
seenIds.add(medico.id);
|
|
||||||
results.push(medico);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Erro na busca com query: ${query}`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results.slice(0, 20); // Limita a 20 resultados
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function buscarMedicoPorId(id: string | number): Promise<Medico> {
|
|
||||||
// Primeiro tenta buscar no Supabase (dados reais)
|
|
||||||
try {
|
|
||||||
const url = `${REST}/doctors?id=eq.${id}`;
|
|
||||||
const res = await fetch(url, { method: "GET", headers: baseHeaders() });
|
|
||||||
const arr = await parse<Medico[]>(res);
|
|
||||||
if (arr && arr.length > 0) {
|
|
||||||
console.log('✅ Médico encontrado no Supabase:', arr[0]);
|
|
||||||
console.log('🔍 Campo especialidade no médico:', {
|
|
||||||
especialidade: arr[0].especialidade,
|
|
||||||
specialty: (arr[0] as any).specialty,
|
|
||||||
hasEspecialidade: !!arr[0].especialidade,
|
|
||||||
hasSpecialty: !!((arr[0] as any).specialty)
|
|
||||||
});
|
|
||||||
return arr[0];
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('⚠️ Erro ao buscar no Supabase, tentando mock API:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Se não encontrar no Supabase, tenta o mock API
|
|
||||||
try {
|
|
||||||
const url = `https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctors/${id}`;
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "GET",
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
if (res.status === 404) {
|
|
||||||
throw new Error("404: Médico não encontrado");
|
|
||||||
}
|
|
||||||
throw new Error(`Erro ao buscar médico: ${res.status} ${res.statusText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const medico = await res.json();
|
|
||||||
console.log('✅ Médico encontrado no Mock API:', medico);
|
|
||||||
return medico as Medico;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erro ao buscar médico em ambas as APIs:', error);
|
|
||||||
throw new Error("404: Médico não encontrado");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dentro de lib/api.ts
|
|
||||||
export async function criarMedico(input: MedicoInput): Promise<Medico> {
|
|
||||||
console.log("Enviando os dados para a API:", input); // Log para depuração
|
|
||||||
|
|
||||||
const url = `${REST}/doctors`; // Endpoint de médicos
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "POST",
|
|
||||||
headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
|
|
||||||
body: JSON.stringify(input), // Enviando os dados padronizados
|
|
||||||
});
|
|
||||||
|
|
||||||
const arr = await parse<Medico[] | Medico>(res); // Resposta da API
|
|
||||||
return Array.isArray(arr) ? arr[0] : (arr as Medico); // Retorno do médico
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export async function atualizarMedico(id: string | number, input: MedicoInput): Promise<Medico> {
|
|
||||||
console.log(`🔄 Tentando atualizar médico ID: ${id}`);
|
|
||||||
console.log(`📤 Payload original:`, input);
|
|
||||||
|
|
||||||
// Criar um payload limpo apenas com campos básicos que sabemos que existem
|
|
||||||
const cleanPayload = {
|
|
||||||
full_name: input.full_name,
|
|
||||||
crm: input.crm,
|
|
||||||
specialty: input.specialty,
|
|
||||||
email: input.email,
|
|
||||||
phone_mobile: input.phone_mobile,
|
|
||||||
cpf: input.cpf,
|
|
||||||
cep: input.cep,
|
|
||||||
street: input.street,
|
|
||||||
number: input.number,
|
|
||||||
city: input.city,
|
|
||||||
state: input.state,
|
|
||||||
active: input.active ?? true
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`📤 Payload limpo:`, cleanPayload);
|
|
||||||
|
|
||||||
// Atualizar apenas no Supabase (dados reais)
|
|
||||||
try {
|
|
||||||
const url = `${REST}/doctors?id=eq.${id}`;
|
|
||||||
console.log(`🌐 URL de atualização: ${url}`);
|
|
||||||
|
|
||||||
const res = await fetch(url, {
|
|
||||||
method: "PATCH",
|
|
||||||
headers: withPrefer({ ...baseHeaders(), "Content-Type": "application/json" }, "return=representation"),
|
|
||||||
body: JSON.stringify(cleanPayload),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`📡 Resposta do servidor: ${res.status} ${res.statusText}`);
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
const arr = await parse<Medico[] | Medico>(res);
|
|
||||||
const result = Array.isArray(arr) ? arr[0] : (arr as Medico);
|
|
||||||
console.log('✅ Médico atualizado no Supabase:', result);
|
|
||||||
return result;
|
|
||||||
} else {
|
|
||||||
// Vamos tentar ver o erro detalhado
|
|
||||||
const errorText = await res.text();
|
|
||||||
console.error(`❌ Erro detalhado do Supabase:`, {
|
|
||||||
status: res.status,
|
|
||||||
statusText: res.statusText,
|
|
||||||
response: errorText,
|
|
||||||
headers: Object.fromEntries(res.headers.entries())
|
|
||||||
});
|
|
||||||
throw new Error(`Supabase error: ${res.status} ${res.statusText} - ${errorText}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Erro ao atualizar médico:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function excluirMedico(id: string | number): Promise<void> {
|
|
||||||
const url = `${REST}/doctors?id=eq.${id}`;
|
|
||||||
const res = await fetch(url, { method: "DELETE", headers: baseHeaders() });
|
|
||||||
await parse<any>(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== CEP (usado nos formulários) =====
|
|
||||||
export async function buscarCepAPI(cep: string): Promise<{
|
|
||||||
logradouro?: string;
|
|
||||||
bairro?: string;
|
|
||||||
localidade?: string;
|
|
||||||
uf?: string;
|
|
||||||
erro?: boolean;
|
|
||||||
}> {
|
|
||||||
const clean = (cep || "").replace(/\D/g, "");
|
|
||||||
try {
|
|
||||||
const res = await fetch(`https://viacep.com.br/ws/${clean}/json/`);
|
|
||||||
const json = await res.json();
|
|
||||||
if (json?.erro) return { erro: true };
|
|
||||||
return {
|
|
||||||
logradouro: json.logradouro ?? "",
|
|
||||||
bairro: json.bairro ?? "",
|
|
||||||
localidade: json.localidade ?? "",
|
|
||||||
uf: json.uf ?? "",
|
|
||||||
erro: false,
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return { erro: true };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Stubs pra não quebrar imports dos forms (sem rotas de storage na doc) =====
|
|
||||||
export async function listarAnexos(_id: string | number): Promise<any[]> { return []; }
|
|
||||||
export async function adicionarAnexo(_id: string | number, _file: File): Promise<any> { return {}; }
|
|
||||||
export async function removerAnexo(_id: string | number, _anexoId: string | number): Promise<void> {}
|
|
||||||
export async function uploadFotoPaciente(_id: string | number, _file: File): Promise<{ foto_url?: string; thumbnail_url?: string }> { return {}; }
|
|
||||||
export async function removerFotoPaciente(_id: string | number): Promise<void> {}
|
|
||||||
export async function listarAnexosMedico(_id: string | number): Promise<any[]> { return []; }
|
|
||||||
export async function adicionarAnexoMedico(_id: string | number, _file: File): Promise<any> { return {}; }
|
|
||||||
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> {}
|
|
||||||
@ -3,7 +3,7 @@
|
|||||||
* Implementa lock para evitar múltiplas chamadas de refresh simultaneamente
|
* Implementa lock para evitar múltiplas chamadas de refresh simultaneamente
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AUTH_STORAGE_KEYS } from '@/types/auth'
|
import { AUTH_STORAGE_KEYS, LOGIN_ROUTES, type UserType } from '@/types/auth'
|
||||||
import { isExpired } from '@/lib/jwt'
|
import { isExpired } from '@/lib/jwt'
|
||||||
import { API_KEY } from '@/lib/config'
|
import { API_KEY } from '@/lib/config'
|
||||||
|
|
||||||
@ -180,14 +180,10 @@ class HttpClient {
|
|||||||
|
|
||||||
// Redirecionar para login
|
// Redirecionar para login
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
const userType = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE) || 'profissional'
|
const storedType = localStorage.getItem(AUTH_STORAGE_KEYS.USER_TYPE)
|
||||||
const loginRoutes = {
|
const userType = (storedType ?? 'profissional') as UserType
|
||||||
profissional: '/login',
|
const loginRoute = LOGIN_ROUTES[userType] ?? '/login'
|
||||||
paciente: '/login-paciente',
|
|
||||||
administrador: '/login-admin'
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginRoute = loginRoutes[userType as keyof typeof loginRoutes] || '/login'
|
|
||||||
window.location.href = loginRoute
|
window.location.href = loginRoute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -254,7 +250,8 @@ class HttpClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Instância única do cliente HTTP
|
// Instância única do cliente HTTP
|
||||||
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'https://mock.apidog.com/m1/1053378-0-default'
|
const envProcess = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process
|
||||||
|
const API_BASE_URL = envProcess?.env?.NEXT_PUBLIC_API_URL || 'https://mock.apidog.com/m1/1053378-0-default'
|
||||||
export const httpClient = new HttpClient(API_BASE_URL)
|
export const httpClient = new HttpClient(API_BASE_URL)
|
||||||
|
|
||||||
export default httpClient
|
export default httpClient
|
||||||
@ -23,3 +23,28 @@ export function validarCPFLocal(cpf: string): boolean {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== CEP (usado nos formulários) =====
|
||||||
|
export async function buscarCepAPI(cep: string): Promise<{
|
||||||
|
logradouro?: string;
|
||||||
|
bairro?: string;
|
||||||
|
localidade?: string;
|
||||||
|
uf?: string;
|
||||||
|
erro?: boolean;
|
||||||
|
}> {
|
||||||
|
const clean = (cep || "").replace(/\D/g, "");
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://viacep.com.br/ws/${clean}/json/`);
|
||||||
|
const json = await res.json();
|
||||||
|
if (json?.erro) return { erro: true };
|
||||||
|
return {
|
||||||
|
logradouro: json.logradouro ?? "",
|
||||||
|
bairro: json.bairro ?? "",
|
||||||
|
localidade: json.localidade ?? "",
|
||||||
|
uf: json.uf ?? "",
|
||||||
|
erro: false,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { erro: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -84,7 +84,7 @@ export const USER_TYPE_ROUTES: UserTypeRoutes = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const LOGIN_ROUTES: LoginRoutes = {
|
export const LOGIN_ROUTES: LoginRoutes = {
|
||||||
profissional: '/login',
|
profissional: '/login?role=profissional',
|
||||||
paciente: '/login-paciente',
|
paciente: '/login?role=paciente',
|
||||||
administrador: '/login-admin',
|
administrador: '/login?role=administrador',
|
||||||
} as const
|
} as const
|
||||||
@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"baseUrl": ".",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@ -22,6 +23,12 @@
|
|||||||
"@/*": ["./*"]
|
"@/*": ["./*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "lib/api.js"],
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
"lib/api.js"
|
||||||
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user