From 7199c107f218319005f938c31a784ef9e436c976 Mon Sep 17 00:00:00 2001 From: EdilbertoC Date: Tue, 28 Apr 2026 10:22:54 -0300 Subject: [PATCH] fix(principal): integra auth, agenda e laudos com a api --- .env.example | 1 + docs/repository-api-audit.md | 109 ++++----- src/App.jsx | 43 +++- src/config/api.js | 79 ++++++- src/mappers/appointmentMapper.js | 61 +++++ src/mappers/reportMapper.js | 73 ++++++ src/pages/AgendaPage.jsx | 64 +++--- src/pages/AuthPages.jsx | 70 ++++-- src/pages/MessagesPage.jsx | 38 +++- src/pages/ProfilePage.jsx | 101 ++++++++- src/pages/ReportsPage.jsx | 92 +++----- src/pages/TeamPage.jsx | 7 +- src/repositories/appointmentRepository.js | 48 +++- src/repositories/authRepository.js | 124 +++++++++++ src/repositories/communicationRepository.js | 44 ++++ src/repositories/patientRepository.js | 81 +++++-- src/repositories/professionalRepository.js | 33 ++- src/repositories/profileRepository.js | 75 ++++++- src/repositories/reportRepository.js | 235 +++++++++++--------- src/repositories/repositoryUtils.js | 74 ++++++ 20 files changed, 1121 insertions(+), 331 deletions(-) create mode 100644 src/mappers/appointmentMapper.js create mode 100644 src/mappers/reportMapper.js create mode 100644 src/repositories/authRepository.js create mode 100644 src/repositories/repositoryUtils.js diff --git a/.env.example b/.env.example index e62b46e..1b61f48 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ VITE_SUPABASE_URL=https://yuanqfswhberkoevtmfr.supabase.co +VITE_API_BASE_URL=https://yuanqfswhberkoevtmfr.supabase.co/functions/v1 VITE_SUPABASE_REST_URL=https://yuanqfswhberkoevtmfr.supabase.co/rest/v1 VITE_SUPABASE_FUNCTIONS_URL=https://yuanqfswhberkoevtmfr.supabase.co/functions/v1 VITE_SUPABASE_STORAGE_URL=https://yuanqfswhberkoevtmfr.supabase.co/storage/v1 diff --git a/docs/repository-api-audit.md b/docs/repository-api-audit.md index 4025e5b..4943b59 100644 --- a/docs/repository-api-audit.md +++ b/docs/repository-api-audit.md @@ -1,82 +1,49 @@ -# Auditoria dos repositories contra a API +# Auditoria de Implementacao e Mapeamento da API -Fonte da documentacao: https://do5wegrct3.apidog.io/llms.txt +Este documento resume o estado atual da integracao entre o front-end e os endpoints da API. -## Endpoints documentados +## Integrado no front -| Grupo | Metodo | Endpoint | Status no frontend | -| --- | --- | --- | --- | -| Autenticacao | POST | `/auth/v1/token?grant_type=password` | Sem repository dedicado | -| Autenticacao | POST | `/auth/v1/otp` | Sem repository dedicado | -| Autenticacao | POST | `/auth/v1/logout` | Sem repository dedicado | -| Autenticacao | GET | `/auth/v1/user` | Sem repository dedicado; relacionado a `profileRepository` | -| Usuarios | POST | `/delete-user` | Sem repository dedicado | -| Usuarios | POST | `/functions/v1/create-user` | Sem repository dedicado | -| Usuarios | POST | `/functions/v1/create-user-with-password` | Sem repository dedicado | -| Usuarios | POST | `/request-password-reset` | Sem repository dedicado | -| Usuarios | POST | `/functions/v1/user-info` | Sem repository dedicado; relacionado a `profileRepository` | -| Usuarios | POST | `/functions/v1/user-info-by-id` | Sem repository dedicado | -| SMS | POST | `/functions/v1/send-sms` | `communicationRepository` nao implementa chamada real | -| Pacientes | GET | `/rest/v1/patients` | Implementado em `patientRepository.getAll` | -| Pacientes | POST | `/rest/v1/patients` | Implementado em `patientRepository.create` | -| Pacientes | PATCH | `/rest/v1/patients?id=eq.{id}` | Implementado em `patientRepository.update` | -| Pacientes | DELETE | `/rest/v1/patients?id=eq.{id}` | Implementado em `patientRepository.remove` | -| Pacientes | POST | `/functions/v1/create-patient` | Implementado em `patientRepository.createWithValidation` | -| Pacientes | POST | `/functions/v1/register-patient` | Nao implementado | -| Medicos | GET | `/rest/v1/doctors` | `professionalRepository.getAll` usa mock | -| Medicos | POST | `/functions/v1/create-doctor` | Nao implementado | -| Agendamentos | GET | `/rest/v1/appointments` | `appointmentRepository.getAll` usa mock | -| Agendamentos | POST | `/rest/v1/appointments` | Nao implementado | -| Agendamentos | POST | `/functions/v1/get-available-slots` | Nao implementado | -| Disponibilidade | GET | `/rest/v1/doctor_availability` | Nao implementado | -| Disponibilidade | POST | `/rest/v1/doctor_availability` | Nao implementado | -| Disponibilidade | PATCH | `/rest/v1/doctor_availability?id=eq.{id}` | Nao implementado | -| Disponibilidade | DELETE | `/rest/v1/doctor_availability?id=eq.{id}` | Nao implementado | -| Disponibilidade | GET | `/rest/v1/doctor_exceptions` | Nao implementado | -| Disponibilidade | POST | `/rest/v1/doctor_exceptions` | Nao implementado | -| Storage | POST | `/storage/v1/object/avatars/{path}` | Nao implementado | -| Storage | GET | `/storage/v1/object/avatars/{path}` | Nao implementado | -| Reports | GET | `/rest/v1/reports` | `reportRepository.getInitialReports` usa mock | -| Reports | POST | `/rest/v1/reports` | Nao implementado | -| Reports | PATCH | `/rest/v1/reports?id=eq.{id}` | Nao implementado | +- **Autenticacao** + - Login com email e senha via Supabase Auth (`/auth/v1/token`). + - Solicitar reset de senha: tenta `/solicitar-reset-de-senha` e usa `/auth/v1/recover` como fallback. + - Dados do usuario autenticado: tenta `/informacoes-do-usuario-autenticado` e usa `/auth/v1/user` como fallback. + - Logout: tenta `/logout`, usa `/auth/v1/logout` como fallback e sempre limpa a sessao local. -## Repositories ainda nao implementados +- **Pacientes** + - Listar, criar, atualizar e deletar pacientes via Supabase REST. + - Criar paciente com validacao via Edge Function quando disponivel. -| Repository | Metodos atuais | Endpoint equivalente | Observacao | -| --- | --- | --- | --- | -| `analyticsRepository` | `getDashboardData` | Nao documentado | Retorna dados estaticos de KPIs, graficos e pacientes frequentes. | -| `appointmentRepository` | `getAll`, `getTodayTimeline`, `getPredictiveQueueSummary`, `getWeekDays` | Parcial: `GET /rest/v1/appointments`, `POST /rest/v1/appointments`, `POST /functions/v1/get-available-slots` | `getAll` deveria chamar a API; demais metodos sao derivados/visuais e nao aparecem na doc. | -| `communicationRepository` | `getCampaigns`, `getInitialMessages`, `getInitialTemplates` | Parcial: `POST /functions/v1/send-sms` | A API so documenta envio de SMS; nao ha endpoints para campanhas, mensagens ou templates. | -| `homeRepository` | `getDashboardOverview` | Nao documentado | Tela inicial usa agregados estaticos. | -| `medicalRecordRepository` | `getRecordTypes`, `getInitialRecords` | Nao documentado | Nao ha endpoint de prontuarios/medical records na doc atual. | -| `professionalRepository` | `getAll`, `getCoverageMap` | Parcial: `GET /rest/v1/doctors`, `POST /functions/v1/create-doctor` | `getAll` deveria usar doctors; `getCoverageMap` parece derivado de disponibilidade, mas nao bate direto com um endpoint. | -| `profileRepository` | `getCurrentUserProfile` | Parcial: `GET /auth/v1/user`, `POST /functions/v1/user-info` | Retorna perfil fixo; deveria consumir dados do usuario autenticado. | -| `reportRepository` | `getAdminUsers`, `getCurrentUser`, `getDoctors`, `getInitialReports`, `getReportTypes`, `getTemplates` | Parcial: `GET/POST/PATCH /rest/v1/reports` | Lista e metadados sao mockados; nao existem metodos de criar/atualizar usando API. | -| `settingsRepository` | `getIntegrations`, `getSections` | Nao documentado | Configuracoes exibidas sao estaticas. | -| `visitRepository` | `getCareQueue`, `getStages` | Nao documentado | Nao ha endpoint de atendimentos/fila/visits na doc atual. | +- **Agendamentos** + - Listar agendamentos: tenta `GET /agendamentos` e usa Supabase REST `appointments` como fallback. + - Criar agendamento: tenta `POST /agendamentos` e usa Supabase REST `appointments` como fallback. -## Inconsistencias encontradas +- **Laudos Medicos** + - Listar relatorios: tenta `GET /reports` e usa Supabase REST `reports` como fallback. + - Criar relatorio: tenta `POST /reports` e usa Supabase REST `reports` como fallback. + - Atualizar relatorio: tenta `PATCH /reports/{id}`, depois `PATCH /reports`, e usa Supabase REST `reports` como fallback. -- `patientRepository.getById(patientId)` nao bate com um endpoint documentado especifico. Ele chama `getAll()` e filtra em memoria; se a API aceitar filtro Supabase por id, o ideal seria usar `/rest/v1/patients?id=eq.{id}&select=*`, mas isso nao aparece como endpoint proprio na documentacao. -- `patientRepository.getDirectoryRows()` transforma pacientes em campos de UI e preenche `insurance`, `city`, `state`, `vip`, `lastVisit` e `nextVisit` com valores fixos. Esses campos nao estao descritos na resposta de `GET /rest/v1/patients`. -- `patientRepository.create(data)` e `createWithValidation(data)` enviam `created_by` com UUID zerado quando nao informado. A documentacao nao confirma esse fallback; isso pode gerar registro invalido se a API exigir usuario real. -- `patientRepository.createWithValidation(data)` usa a Edge Function documentada (`/functions/v1/create-patient`), mas a API tambem possui o endpoint publico `/functions/v1/register-patient`, ainda sem metodo correspondente. -- `appointmentRepository.getAll()` nao chama `GET /rest/v1/appointments`; usa `mockData`. Alem disso, nao existem metodos para `POST /rest/v1/appointments` nem para `POST /functions/v1/get-available-slots`. -- `professionalRepository.getAll()` nao chama `GET /rest/v1/doctors`; usa `mockData`. Tambem falta metodo para `POST /functions/v1/create-doctor`. -- `reportRepository.getInitialReports()` nao chama `GET /rest/v1/reports`; usa dados estaticos e nomes/status em portugues (`rascunho`, `finalizado`, `enviado`) diferentes dos status documentados (`draft`, `completed`). -- `reportRepository` expoe templates, tipos, usuarios admin e medico atual, mas esses recursos nao aparecem na API documentada de Reports. -- `communicationRepository` tem campanhas, mensagens e templates, mas a documentacao so possui `POST /functions/v1/send-sms`; nao ha equivalencia para listar ou gerenciar esses dados. -- `profileRepository.getCurrentUserProfile()` retorna perfil fixo; deveria ser alinhado com `GET /auth/v1/user` ou `POST /functions/v1/user-info`. -- `homeRepository`, `analyticsRepository`, `settingsRepository`, `visitRepository` e `medicalRecordRepository` nao possuem endpoints equivalentes na documentacao atual. +- **Medicos / Profissionais** + - Listar medicos: tenta `GET /listar-medicos` e usa Supabase REST `doctors` como fallback. -## Configuracao extraida +- **Mensageria** + - Enviar SMS: tenta `POST /enviar-sms-via-twilio` e usa Edge Function `send-sms` como fallback. + - O formulario agora coleta telefone quando o canal selecionado e SMS. -As variaveis reutilizaveis de API foram centralizadas em `src/config/api.js`: +- **Storage** + - Upload de avatar: tenta `/upload-avatar` e usa Supabase Storage no bucket `avatars` como fallback. + - A tela de perfil atualiza a imagem exibida apos upload bem-sucedido. -- `VITE_SUPABASE_URL` -- `VITE_SUPABASE_REST_URL` -- `VITE_SUPABASE_FUNCTIONS_URL` -- `VITE_SUPABASE_STORAGE_URL` -- `VITE_SUPABASE_ANON_KEY` +## Ainda sem endpoint consolidado documentado -O arquivo mantem os valores atuais como fallback para nao quebrar o ambiente local, mas o ideal e configurar esses valores via `.env`. +- Dashboard / Inicio (`HomePage` / `homeRepository.js`). +- Estatisticas e BI (`AnalyticsPage` / `analyticsRepository.js`). +- Prontuarios especificos separados de laudos (`MedicalRecordsPage` / `medicalRecordRepository.js`). +- Consultas isoladas fora de agendamento (`VisitsPage` / `visitRepository.js`). +- Configuracoes gerais do tenant (`SettingsPage` / `settingsRepository.js`). + +## Observacoes + +- `VITE_API_BASE_URL` define a base dos endpoints nomeados da API. Quando nao informado, o front usa `VITE_SUPABASE_FUNCTIONS_URL`. +- Os reposititorios aceitam formatos de resposta comuns como arrays diretos ou objetos com chaves `data`, `reports`, `agendamentos`, `medicos` etc. +- Os fallbacks existem para manter o front funcional em ambientes onde parte das Edge Functions ainda nao foi publicada. diff --git a/src/App.jsx b/src/App.jsx index 790bdc6..2a9c4ed 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,5 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' +import { authRepository } from './repositories/authRepository.js' + import './App.css' import { AppShell } from './components/AppShell.jsx' import { AgendaPage } from './pages/AgendaPage.jsx' @@ -48,11 +50,16 @@ function App() { }, []) const route = useMemo(() => resolveRoute(location.pathname, navigate), [location.pathname, navigate]) + const isAuthenticated = authRepository.isAuthenticated() if (!route.withShell) { return route.element } + if (!isAuthenticated) { + return + } + return ( {route.element} @@ -119,15 +126,10 @@ function resolveRoute(pathname, navigate) { if (pathname.startsWith('/pacientes/')) { const patientId = pathname.split('/')[2] - const patient = patientRepository.getById(patientId) return { - element: patient ? ( - - ) : ( - - ), - title: patient?.name || 'Paciente nao encontrado', + element: , + title: 'Paciente', withShell: true, } } @@ -195,6 +197,33 @@ function resolveRoute(pathname, navigate) { } } +function PatientDetailRoute({ navigate, patientId }) { + const [patient, setPatient] = useState(null) + const [loading, setLoading] = useState(true) + + useEffect(() => { + let active = true + + patientRepository.getById(patientId) + .then((data) => { + if (active) setPatient(data) + }) + .finally(() => { + if (active) setLoading(false) + }) + + return () => { + active = false + } + }, [patientId]) + + if (loading) { + return Carregando paciente... + } + + return patient ? : +} + function readLocation() { return { pathname: normalizePath(window.location.pathname), diff --git a/src/config/api.js b/src/config/api.js index 33de4e8..f83434c 100644 --- a/src/config/api.js +++ b/src/config/api.js @@ -1,7 +1,10 @@ const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL || 'https://yuanqfswhberkoevtmfr.supabase.co' const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ' +const AUTH_SESSION_KEY = 'mediconnect.auth.session' + export const apiConfig = { + apiUrl: import.meta.env.VITE_API_BASE_URL || import.meta.env.VITE_SUPABASE_FUNCTIONS_URL || `${SUPABASE_URL}/functions/v1`, supabaseUrl: SUPABASE_URL, restUrl: import.meta.env.VITE_SUPABASE_REST_URL || `${SUPABASE_URL}/rest/v1`, functionsUrl: import.meta.env.VITE_SUPABASE_FUNCTIONS_URL || `${SUPABASE_URL}/functions/v1`, @@ -9,8 +12,76 @@ export const apiConfig = { anonKey: SUPABASE_ANON_KEY, } -export const apiHeaders = { - apikey: apiConfig.anonKey, - Authorization: `Bearer ${apiConfig.anonKey}`, - 'Content-Type': 'application/json', +export function apiEndpoint(path, baseUrl = apiConfig.apiUrl) { + const normalizedBase = baseUrl.replace(/\/+$/, '') + const normalizedPath = path.startsWith('/') ? path : `/${path}` + return `${normalizedBase}${normalizedPath}` +} + +export function getAuthSession() { + if (typeof window === 'undefined') return null + const rawSession = window.sessionStorage.getItem(AUTH_SESSION_KEY) + if (!rawSession) return null + + try { + return JSON.parse(rawSession) + } catch { + clearAuthSession() + return null + } +} + +export function saveAuthSession(session) { + if (typeof window !== 'undefined') { + window.sessionStorage.setItem(AUTH_SESSION_KEY, JSON.stringify(session)) + } +} + +export function clearAuthSession() { + if (typeof window !== 'undefined') { + window.sessionStorage.removeItem(AUTH_SESSION_KEY) + } +} + +export function hasAuthenticatedSession() { + const session = getAuthSession() + if (!session?.access_token) return false + + // Validate expiration locally if available + if (session.expires_at && session.expires_at * 1000 <= Date.now()) { + clearAuthSession() + return false + } + + return true +} + +export function getAnonHeaders(extraHeaders = {}) { + return cleanHeaders({ + apikey: apiConfig.anonKey, + 'Content-Type': 'application/json', + ...extraHeaders, + }) +} + +export function getAuthenticatedHeaders(extraHeaders = {}) { + const session = getAuthSession() + const accessToken = session?.access_token + + if (!accessToken) { + throw new Error('Sessão expirada. Faça login novamente.') + } + + return cleanHeaders({ + apikey: apiConfig.anonKey, + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + ...extraHeaders, + }) +} + +function cleanHeaders(headers) { + return Object.fromEntries( + Object.entries(headers).filter(([, value]) => value !== undefined && value !== null), + ) } diff --git a/src/mappers/appointmentMapper.js b/src/mappers/appointmentMapper.js new file mode 100644 index 0000000..61e7c38 --- /dev/null +++ b/src/mappers/appointmentMapper.js @@ -0,0 +1,61 @@ +export const appointmentMapper = { + toUi(apiData) { + if (!apiData) return null + + const patient = apiData.patient || apiData.paciente || apiData.patients || {} + const professional = apiData.doctor || apiData.medico || apiData.professional || apiData.doctors || {} + + return { + id: apiData.id || apiData.agendamento_id, + patientId: apiData.patientId || apiData.patient_id || apiData.paciente_id || patient.id, + patient: apiData.patientName || apiData.patient_name || patient.full_name || patient.nome || patient.name || 'Paciente', + professional: + apiData.professional || + apiData.professionalName || + apiData.doctor_name || + apiData.medico_nome || + professional.name || + professional.nome || + 'Medico(a)', + date: apiData.date || apiData.data || apiData.appointment_date || apiData.data_agendamento || '', + time: apiData.time || apiData.hora || apiData.appointment_time || apiData.horario || '', + type: apiData.type || apiData.tipo || apiData.tipo_consulta || 'Consulta', + mode: apiData.mode || apiData.modalidade || apiData.formato || 'Presencial', + status: apiData.status || apiData.situacao || 'Aguardando', + room: apiData.room || apiData.sala || apiData.local || 'Consultorio 1', + } + }, + + toApi(uiData, dialect = 'api') { + if (dialect === 'supabase') { + return { + patient_id: uiData.patientId, + doctor_id: uiData.professionalId || null, + appointment_date: uiData.date, + appointment_time: uiData.time, + type: uiData.type, + mode: uiData.mode, + status: uiData.status || 'Confirmada', + room: uiData.room, + } + } + + return { + patient_id: uiData.patientId, + paciente_id: uiData.patientId, + doctor_id: uiData.professionalId || null, + medico_id: uiData.professionalId || null, + appointment_date: uiData.date, + data: uiData.date, + appointment_time: uiData.time, + hora: uiData.time, + type: uiData.type, + tipo: uiData.type, + mode: uiData.mode, + modalidade: uiData.mode, + status: uiData.status || 'Confirmada', + room: uiData.room, + sala: uiData.room, + } + }, +} diff --git a/src/mappers/reportMapper.js b/src/mappers/reportMapper.js new file mode 100644 index 0000000..1f9f01f --- /dev/null +++ b/src/mappers/reportMapper.js @@ -0,0 +1,73 @@ +export const reportMapper = { + toUi(apiData) { + if (!apiData) return null + + const patient = apiData.patient || apiData.paciente || apiData.patients || {} + const doctor = apiData.doctor || apiData.medico || apiData.professional || apiData.doctors || {} + const createdAt = apiData.created_at || apiData.createdAt || apiData.data_criacao || apiData.date + const status = normalizeStatus(apiData.status || apiData.situacao) + + return { + id: String(apiData.id || apiData.report_id || apiData.laudo_id), + patientId: apiData.patientId || apiData.patient_id || apiData.paciente_id || patient.id || '', + patient: apiData.patientName || apiData.patient_name || patient.full_name || patient.nome || patient.name || 'Paciente', + date: createdAt ? new Date(createdAt).toLocaleDateString('pt-BR') : 'Sem data', + doctor: apiData.doctorName || apiData.doctor_name || apiData.medico_nome || doctor.name || doctor.nome || 'Medico(a)', + author: apiData.author || apiData.autor || doctor.name || doctor.nome || 'Medico(a)', + type: apiData.type || apiData.report_type || apiData.tipo || apiData.tipo_laudo || 'Laudo medico', + status, + content: apiData.content || apiData.conteudo || apiData.text || '', + cid: apiData.cid || '', + tags: apiData.tags || [], + verified: apiData.verified ?? apiData.verificado ?? status !== 'rascunho', + showDate: apiData.showDate ?? apiData.exibir_data ?? true, + signDigital: apiData.signDigital ?? apiData.assinatura_digital ?? true, + versions: normalizeVersions(apiData.versions || apiData.versoes), + } + }, + + toApi(uiData, dialect = 'api') { + if (dialect === 'supabase') { + return { + patient_id: uiData.patientId, + report_type: uiData.type, + content: uiData.content, + status: uiData.status, + cid: uiData.cid || null, + } + } + + return { + patient_id: uiData.patientId, + paciente_id: uiData.patientId, + report_type: uiData.type, + tipo: uiData.type, + content: uiData.content, + conteudo: uiData.content, + status: uiData.status, + cid: uiData.cid || null, + } + }, +} + +function normalizeStatus(status) { + if (!status) return 'rascunho' + + const normalized = String(status).toLowerCase() + if (['finalizado', 'liberado', 'assinado'].includes(normalized)) return 'finalizado' + if (['enviado', 'entregue'].includes(normalized)) return 'enviado' + return 'rascunho' +} + +function normalizeVersions(versions) { + if (Array.isArray(versions) && versions.length) return versions + + return [ + { + version: 1, + action: 'Criado', + user: 'Sistema', + summary: 'Registro importado da API', + }, + ] +} diff --git a/src/pages/AgendaPage.jsx b/src/pages/AgendaPage.jsx index 1cace68..b9f3b2c 100644 --- a/src/pages/AgendaPage.jsx +++ b/src/pages/AgendaPage.jsx @@ -16,30 +16,38 @@ const viewFilters = ['Dia', 'Semana', 'Mês'] export function AgendaPage({ navigate }) { const [patients, setPatients] = useState([]) - const professionals = professionalRepository.getAll() + const [professionals, setProfessionals] = useState([]) const queue = appointmentRepository.getPredictiveQueueSummary() const timeline = appointmentRepository.getTodayTimeline() const weekDays = appointmentRepository.getWeekDays() const [activeView, setActiveView] = useState('Dia') const [status, setStatus] = useState('Todos') const [modalOpen, setModalOpen] = useState(false) - const [localAppointments, setLocalAppointments] = useState(() => appointmentRepository.getAll()) + const [localAppointments, setLocalAppointments] = useState([]) const [form, setForm] = useState({ patientId: '', - professional: professionals[0]?.name || '', + professionalId: '', type: 'Retorno', time: '15:30', mode: 'Teleconsulta', }) useEffect(() => { - patientRepository.getAll().then((data) => { - setPatients(data) + Promise.all([ + patientRepository.getAll(), + appointmentRepository.getAll(), + professionalRepository.getAll() + ]).then(([patientsData, appointmentsData, professionalsData]) => { + setPatients(patientsData) + setLocalAppointments(appointmentsData || []) + setProfessionals(professionalsData || []) + setForm((current) => ({ ...current, - patientId: data[0]?.id || '', + patientId: patientsData?.length ? patientsData[0].id : '', + professionalId: professionalsData?.length ? professionalsData[0].id : '', })) - }) + }).catch(e => console.error(e)) }, []) const visibleAppointments = useMemo(() => { @@ -54,26 +62,28 @@ useEffect(() => { setForm((current) => ({ ...current, [field]: value })) } - function handleCreate(event) { + async function handleCreate(event) { event.preventDefault() - const patient = patients.find((item) => item.id === form.patientId) || patients[0] - - setLocalAppointments((current) => [ - ...current, - { - id: `apt-local-${current.length + 1}`, - date: '2026-04-07', - patient: patient.name, - patientId: patient.id, - professional: form.professional, - room: form.mode === 'Teleconsulta' ? 'Sala virtual 3' : 'Sala 02', - status: 'Confirmada', + + // Fallback date and time + const today = new Date().toISOString().split('T')[0] + + try { + const created = await appointmentRepository.create({ + patientId: form.patientId, + date: today, time: form.time, type: form.type, mode: form.mode, - }, - ]) - setModalOpen(false) + room: form.mode === 'Teleconsulta' ? 'Virtual' : 'Consultório 1', + professionalId: form.professionalId, + }) + + setLocalAppointments((current) => [...current, created]) + setModalOpen(false) + } catch(err) { + alert(err.message || 'Erro ao criar agendamento.') + } } return ( @@ -244,7 +254,7 @@ useEffect(() => { > {patients.map((patient) => ( - {patient.name} + {patient.name || patient.full_name || patient.nome} ))} @@ -274,11 +284,11 @@ useEffect(() => { updateForm('professional', event.target.value)} - value={form.professional} + onChange={(event) => updateForm('professionalId', event.target.value)} + value={form.professionalId} > {professionals.map((professional) => ( - {professional.name} + {professional.name} ))} diff --git a/src/pages/AuthPages.jsx b/src/pages/AuthPages.jsx index 02ff71d..ff8d741 100644 --- a/src/pages/AuthPages.jsx +++ b/src/pages/AuthPages.jsx @@ -1,5 +1,7 @@ import { useState } from 'react' +import { authRepository } from '../repositories/authRepository.js' + import { BrandLogo } from '../components/Brand.jsx' import loginClinicImage from '../assets/figma/login-clinic.png' @@ -9,14 +11,26 @@ export function LoginPage({ navigate }) { password: '', }) const [showPassword, setShowPassword] = useState(false) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') function updateField(field, value) { setForm((current) => ({ ...current, [field]: value })) } - function handleSubmit(event) { + async function handleSubmit(event) { event.preventDefault() - navigate('/inicio') + setLoading(true) + setError('') + + try { + await authRepository.login(form) + navigate('/inicio') + } catch (err) { + setError(err.message || 'Erro de autenticação') + } finally { + setLoading(false) + } } return ( @@ -74,6 +88,12 @@ export function LoginPage({ navigate }) {
{profile.role}
{avatarError}