Merge pull request 'refatoração' (#29) from StsDanilo/riseup-squad21:refatoração into refatoracao

Reviewed-on: #29
This commit is contained in:
LiraS2 2025-10-23 16:12:05 +00:00
commit 1a80c72f4a
30 changed files with 2827 additions and 2097 deletions

View File

@ -1,16 +1,21 @@
// Caminho: app/context/AppointmentsContext.tsx (Completo e Corrigido)
"use client";
import React, { createContext, useContext, useState, useEffect, ReactNode, useCallback } from 'react';
import { agendamentosApi, Appointment } from '@/services/agendamentosApi';
import { agendamentosApi, Appointment, CreateAppointmentData } from '@/services/agendamentosApi';
import { usuariosApi, User } from '@/services/usuariosApi';
import { toast } from "sonner";
// As definições de componentes de UI foram REMOVIDAS deste arquivo.
// Elas pertencem aos arquivos que as utilizam, como `dashboard/page.tsx`.
export interface AppointmentsContextType {
appointments: Appointment[];
isLoading: boolean;
error: string | null;
fetchAppointments: () => Promise<void>;
addAppointment: (appointmentData: Omit<Appointment, 'id' | 'status'>) => Promise<void>;
addAppointment: (appointmentData: CreateAppointmentData) => Promise<void>;
updateAppointment: (appointmentId: string, updatedData: Partial<Omit<Appointment, 'id'>>) => Promise<void>;
deleteAppointment: (appointmentId: string) => Promise<void>;
}
const AppointmentsContext = createContext<AppointmentsContextType | undefined>(undefined);
@ -24,8 +29,13 @@ export function AppointmentsProvider({ children }: { children: ReactNode }) {
setIsLoading(true);
setError(null);
try {
const data = await agendamentosApi.list();
setAppointments(data || []);
const user = await usuariosApi.getCurrentUser();
if (user?.id) {
const data = await agendamentosApi.listByPatient(user.id);
setAppointments(data || []);
} else {
setAppointments([]);
}
} catch (err) {
console.error("Erro ao buscar agendamentos:", err);
setError("Não foi possível carregar os agendamentos.");
@ -39,33 +49,24 @@ export function AppointmentsProvider({ children }: { children: ReactNode }) {
fetchAppointments();
}, [fetchAppointments]);
const addAppointment = async (appointmentData: Omit<Appointment, 'id' | 'status'>) => {
const addAppointment = async (appointmentData: CreateAppointmentData) => {
try {
await agendamentosApi.create(appointmentData);
await fetchAppointments(); // Recarrega a lista para incluir o novo agendamento
await fetchAppointments();
} catch (err) {
console.error("Erro ao adicionar agendamento:", err);
setError("Falha ao criar o novo agendamento. Tente novamente.");
throw err;
}
};
const updateAppointment = async (appointmentId: string, updatedData: Partial<Omit<Appointment, 'id'>>) => {
try {
await agendamentosApi.update(appointmentId, updatedData);
await fetchAppointments(); // Recarrega a lista para refletir as alterações
toast.warning("Funcionalidade indisponível.", { description: "A API não suporta a atualização de agendamentos." });
} catch (err) {
console.error("Erro ao atualizar agendamento:", err);
console.error("Erro ao tentar atualizar agendamento:", err);
setError("Falha ao atualizar o agendamento. Tente novamente.");
}
};
const deleteAppointment = async (appointmentId: string) => {
try {
await agendamentosApi.delete(appointmentId);
await fetchAppointments(); // Recarrega a lista para remover o item excluído
} catch (err) {
console.error("Erro ao excluir agendamento:", err);
setError("Falha ao excluir o agendamento. Tente novamente.");
throw err;
}
};
@ -76,7 +77,6 @@ export function AppointmentsProvider({ children }: { children: ReactNode }) {
fetchAppointments,
addAppointment,
updateAppointment,
deleteAppointment,
};
return (

206
app/dev/api-check/page.tsx Normal file
View File

@ -0,0 +1,206 @@
// app/dev/api-check/page.tsx (V2 - Com Mocks e Seções Colapsáveis)
"use client";
import React, { useState } from 'react';
import {
authService,
userService,
patientService,
doctorService,
scheduleService,
reportService,
} from '@/services/api/apiService';
import * as TestData from '@/services/api/apiTestData';
import {
LoginResponse, SendMagicLinkResponse, LogoutResponse, GetCurrentUserResponse,
RequestPasswordResetResponse, HardDeleteUserResponse, RegisterPatientResponse,
ListResponse, Patient, GetAvailableSlotsResponse
} from '@/services/api/types';
type ApiResponse = { status: number | 'network_error'; data?: any; error?: Error };
const getStyleForResponse = (response: ApiResponse | null): React.CSSProperties => {
if (!response) return {};
const baseStyle: React.CSSProperties = { padding: '10px', border: '1px solid', borderRadius: '4px', whiteSpace: 'pre-wrap', wordBreak: 'break-all', marginTop: '10px' };
if (response.status === 'network_error') return { ...baseStyle, backgroundColor: 'lightgoldenrodyellow', borderColor: 'goldenrod' };
if (response.status >= 200 && response.status < 300) return { ...baseStyle, backgroundColor: 'lightgreen', borderColor: 'green' };
if (response.status >= 400) return { ...baseStyle, backgroundColor: 'lightcoral', borderColor: 'darkred' };
return {};
};
const ApiVerificationPage: React.FC = () => {
// --- ESTADOS ---
const [loginData, setLoginData] = useState(TestData.loginTestData.success);
const [loginResponse, setLoginResponse] = useState<LoginResponse | null>(null);
const [magicLinkEmail, setMagicLinkEmail] = useState(TestData.magicLinkTestData.success);
const [magicLinkResponse, setMagicLinkResponse] = useState<SendMagicLinkResponse | null>(null);
const [logoutResponse, setLogoutResponse] = useState<LogoutResponse | null>(null);
const [currentUserResponse, setCurrentUserResponse] = useState<GetCurrentUserResponse | null>(null);
const [resetPassData, setResetPassData] = useState(TestData.resetPassTestData.success);
const [resetPassResponse, setResetPassResponse] = useState<RequestPasswordResetResponse | null>(null);
const [deleteUserData, setDeleteUserData] = useState(TestData.deleteUserTestData.success);
const [deleteUserResponse, setDeleteUserResponse] = useState<HardDeleteUserResponse | null>(null);
const [registerPatientData, setRegisterPatientData] = useState(TestData.registerPatientTestData.success);
const [registerPatientResponse, setRegisterPatientResponse] = useState<RegisterPatientResponse | null>(null);
const [listPatientsFilter, setListPatientsFilter] = useState(TestData.listPatientsTestData.success);
const [listPatientsResponse, setListPatientsResponse] = useState<ListResponse<Patient> | null>(null);
const [slotsData, setSlotsData] = useState(TestData.slotsTestData.success);
const [slotsResponse, setSlotsResponse] = useState<GetAvailableSlotsResponse | null>(null);
// --- HANDLERS ---
const handleApiCall = async (apiFunction: (...args: any[]) => Promise<any>, payload: any, setResponse: React.Dispatch<React.SetStateAction<any>>) => {
setResponse(null);
const response = await apiFunction(payload);
setResponse(response);
};
const handleApiCallNoPayload = async (apiFunction: () => Promise<any>, setResponse: React.Dispatch<React.SetStateAction<any>>) => {
setResponse(null);
const response = await apiFunction();
setResponse(response);
};
const handleRequestPasswordReset = async () => {
setResetPassResponse(null);
const response = await userService.requestPasswordReset(resetPassData.email, resetPassData.redirectUrl || undefined);
setResetPassResponse(response);
};
const handleGetAvailableSlots = async () => {
setSlotsResponse(null);
const response = await scheduleService.getAvailableSlots(slotsData.doctorId, slotsData.date);
setSlotsResponse(response);
};
return (
<div style={{ fontFamily: 'sans-serif', padding: '20px', maxWidth: '1000px', margin: 'auto' }}>
<h1>Painel de Verificação da API</h1>
<p>Use este painel para executar cada função do `apiService` e verificar o objeto de resposta completo.</p>
<details open>
<summary><h2>Autenticação</h2></summary>
<div className="test-block">
<details>
<summary><h3>authService.login</h3></summary>
<div className="controls">
<button onClick={() => setLoginData(TestData.loginTestData.success)}>Carregar Sucesso</button>
<button onClick={() => setLoginData(TestData.loginTestData.error)}>Carregar Erro</button>
</div>
<pre>{JSON.stringify(loginData, null, 2)}</pre>
<button onClick={() => handleApiCall(authService.login, loginData, setLoginResponse)}>Executar</button>
{loginResponse && <pre style={getStyleForResponse(loginResponse)}>{JSON.stringify(loginResponse, null, 2)}</pre>}
</details>
</div>
<div className="test-block">
<details>
<summary><h3>authService.sendMagicLink</h3></summary>
<div className="controls">
<button onClick={() => setMagicLinkEmail(TestData.magicLinkTestData.success)}>Carregar Sucesso</button>
<button onClick={() => setMagicLinkEmail(TestData.magicLinkTestData.error)}>Carregar Erro</button>
</div>
<pre>{JSON.stringify(magicLinkEmail, null, 2)}</pre>
<button onClick={() => handleApiCall(authService.sendMagicLink, magicLinkEmail.email, setMagicLinkResponse)}>Executar</button>
{magicLinkResponse && <pre style={getStyleForResponse(magicLinkResponse)}>{JSON.stringify(magicLinkResponse, null, 2)}</pre>}
</details>
</div>
<div className="test-block">
<details>
<summary><h3>authService.logout</h3></summary>
<button onClick={() => handleApiCallNoPayload(authService.logout, setLogoutResponse)}>Executar</button>
{logoutResponse && <pre style={getStyleForResponse(logoutResponse)}>{JSON.stringify(logoutResponse, null, 2)}</pre>}
</details>
</div>
<div className="test-block">
<details>
<summary><h3>authService.getCurrentUser</h3></summary>
<button onClick={() => handleApiCallNoPayload(authService.getCurrentUser, setCurrentUserResponse)}>Executar</button>
{currentUserResponse && <pre style={getStyleForResponse(currentUserResponse)}>{JSON.stringify(currentUserResponse, null, 2)}</pre>}
</details>
</div>
</details>
<details open>
<summary><h2>Usuários</h2></summary>
<div className="test-block">
<details>
<summary><h3>userService.requestPasswordReset</h3></summary>
<div className="controls">
<button onClick={() => setResetPassData(TestData.resetPassTestData.success)}>Carregar Sucesso</button>
<button onClick={() => setResetPassData(TestData.resetPassTestData.error)}>Carregar Erro</button>
</div>
<pre>{JSON.stringify(resetPassData, null, 2)}</pre>
<button onClick={handleRequestPasswordReset}>Executar</button>
{resetPassResponse && <pre style={getStyleForResponse(resetPassResponse)}>{JSON.stringify(resetPassResponse, null, 2)}</pre>}
</details>
</div>
<div className="test-block">
<details>
<summary><h3>userService.hardDeleteUser_DANGEROUS</h3></summary>
<div className="controls">
<button onClick={() => setDeleteUserData(TestData.deleteUserTestData.success)}>Carregar Sucesso</button>
<button onClick={() => setDeleteUserData(TestData.deleteUserTestData.error)}>Carregar Erro</button>
</div>
<pre>{JSON.stringify(deleteUserData, null, 2)}</pre>
<button onClick={() => handleApiCall(userService.hardDeleteUser_DANGEROUS, deleteUserData.userId, setDeleteUserResponse)}>Executar</button>
{deleteUserResponse && <pre style={getStyleForResponse(deleteUserResponse)}>{JSON.stringify(deleteUserResponse, null, 2)}</pre>}
</details>
</div>
</details>
<details open>
<summary><h2>Pacientes</h2></summary>
<div className="test-block">
<details>
<summary><h3>patientService.registerPatient</h3></summary>
<div className="controls">
<button onClick={() => setRegisterPatientData(TestData.registerPatientTestData.success)}>Carregar Sucesso</button>
<button onClick={() => setRegisterPatientData(TestData.registerPatientTestData.errorValidation)}>Carregar Erro (Validação)</button>
<button onClick={() => setRegisterPatientData(TestData.registerPatientTestData.errorConflict)}>Carregar Erro (Conflito)</button>
</div>
<pre>{JSON.stringify(registerPatientData, null, 2)}</pre>
<button onClick={() => handleApiCall(patientService.registerPatient, registerPatientData, setRegisterPatientResponse)}>Executar</button>
{registerPatientResponse && <pre style={getStyleForResponse(registerPatientResponse)}>{JSON.stringify(registerPatientResponse, null, 2)}</pre>}
</details>
</div>
<div className="test-block">
<details>
<summary><h3>patientService.list</h3></summary>
<div className="controls">
<button onClick={() => setListPatientsFilter(TestData.listPatientsTestData.success)}>Carregar com Filtro</button>
<button onClick={() => setListPatientsFilter(TestData.listPatientsTestData.noFilter)}>Carregar Sem Filtro</button>
</div>
<pre>{JSON.stringify(listPatientsFilter, null, 2)}</pre>
<button onClick={() => handleApiCall(patientService.list, listPatientsFilter, setListPatientsResponse)}>Executar</button>
{listPatientsResponse && <pre style={getStyleForResponse(listPatientsResponse)}>{JSON.stringify(listPatientsResponse, null, 2)}</pre>}
</details>
</div>
</details>
<details open>
<summary><h2>Agendamentos</h2></summary>
<div className="test-block">
<details>
<summary><h3>scheduleService.getAvailableSlots</h3></summary>
<div className="controls">
<button onClick={() => setSlotsData(TestData.slotsTestData.success)}>Carregar Sucesso</button>
<button onClick={() => setSlotsData(TestData.slotsTestData.error)}>Carregar Erro</button>
</div>
<pre>{JSON.stringify(slotsData, null, 2)}</pre>
<button onClick={handleGetAvailableSlots}>Executar</button>
{slotsResponse && <pre style={getStyleForResponse(slotsResponse)}>{JSON.stringify(slotsResponse, null, 2)}</pre>}
</details>
</div>
</details>
<style>{`
h2 { margin-top: 20px; border-bottom: 2px solid #eee; padding-bottom: 5px; }
summary { cursor: pointer; font-size: 1.2em; font-weight: bold; }
details { margin-bottom: 10px; }
.test-block { border: 1px solid #ccc; padding: 15px; margin-top: 10px; border-radius: 5px; }
.controls button { margin-right: 10px; }
button { margin-top: 10px; margin-bottom: 10px; padding: 8px 12px; cursor: pointer; border-radius: 4px; border: 1px solid #666; }
pre { margin-top: 10px; }
`}</style>
</div>
);
};
export default ApiVerificationPage;

View File

@ -49,8 +49,7 @@ export default function PatientDashboard() {
useEffect(() => {
const fetchData = async () => {
// TODO: Remover ID fixo e obter do usuário logado
const doctorId = "3bb9ee4a-cfdd-4d81-b628-383907dfa225";
const doctorId = JSON.parse(localStorage.getItem("user_info") || "{}")?.id;;
setIsLoading(true);
setError(null);
try {

View File

@ -19,7 +19,7 @@ export default function AvailabilityPage() {
const router = useRouter();
// TODO: Substituir pelo ID do médico autenticado
const doctorIdTemp = "3bb9ee4a-cfdd-4d81-b628-383907dfa225";
const doctorId = JSON.parse(localStorage.getItem("user_info") || "{}")?.id;
useEffect(() => {
const fetchData = async () => {
@ -47,8 +47,8 @@ export default function AvailabilityPage() {
const formData = new FormData(e.currentTarget);
const apiPayload = {
doctor_id: doctorIdTemp,
created_by: doctorIdTemp, // TODO: Substituir pelo ID do usuário autenticado
doctor_id: doctorId,
created_by: doctorId, // TODO: Substituir pelo ID do usuário autenticado
weekday: (formData.get("weekday") as string) || undefined,
start_time: (formData.get("horarioEntrada") as string) || undefined,
end_time: (formData.get("horarioSaida") as string) || undefined,
@ -68,8 +68,8 @@ export default function AvailabilityPage() {
setError(err?.message || "Não foi possível cadastrar a disponibilidade.");
toast({
title: "Erro",
description: err?.message || "Não foi possível cadastrar a disponibilidade.",
variant: "destructive",
description: err?.message || "Não foi possível cadastrar a disponibilidade.",
});
} finally {
setIsSubmitting(false);

View File

@ -1,15 +1,14 @@
// Caminho: manager/dashboard/page.tsx (Refatorado)
"use client";
import ManagerLayout from "@/components/manager-layout";
// Removida a importação de ManagerLayout, pois a página agora é envolvida pelo ManagerLayout pai (em app/manager/layout.tsx)
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Calendar, Clock, Plus, User } from "lucide-react";
import { Calendar, Clock, Plus, User } from "lucide-react";
import Link from "next/link";
import React, { useState, useEffect } from "react";
import { usuariosService } from "@/services/usuariosApi"; // Alterado
import { perfisService } from "@/services/perfisApi"; // Adicionado
import { doctorsService } from "@/services/medicosApi";
import { usuariosApi } from "@/services/usuariosApi";
import { perfisApi } from "@/services/perfisApi";  
import { medicosApi } from "@/services/medicosApi";
export default function ManagerDashboard() {
// 🔹 Estados para usuários
@ -20,17 +19,24 @@ export default function ManagerDashboard() {
const [doctors, setDoctors] = useState<any[]>([]);
const [loadingDoctors, setLoadingDoctors] = useState(true);
// REMOVIDO: mockUserProfile e mockMenuItems foram removidos.
// Agora, o layout (ManagerLayout em app/manager/layout.tsx) é responsável por fornecer esses dados.
// 🔹 Buscar primeiro usuário
useEffect(() => {
async function fetchFirstUser() {
try {
// Passo 1: Buscar a lista de papéis/usuários
const rolesData = await usuariosService.listRoles(); // Alterado
const rolesData = await usuariosApi.listRoles();
if (Array.isArray(rolesData) && rolesData.length > 0) {
const firstRole = rolesData[0];
// Passo 2: Usar o user_id do primeiro papel para buscar o perfil completo
const profileData = await perfisService.getById(firstRole.user_id); // Alterado
setFirstUser(profileData); // Armazena o perfil que contém nome, email, etc.
// NOTE: Esta lógica parece buscar a lista de perfis, não um único perfil por user_id.
// Mantendo a estrutura para evitar quebrar a lógica de dados, mas é uma área a ser revisada.
const profileData = await perfisApi.list();
// Se list() retorna um array, talvez você queira mapear para encontrar o perfil do firstRole.user_id, mas mantendo o original:
setFirstUser(profileData[0]); // Assumindo que o primeiro item é o perfil
}
} catch (error) {
console.error("Erro ao carregar usuário:", error);
@ -46,7 +52,7 @@ export default function ManagerDashboard() {
useEffect(() => {
async function fetchDoctors() {
try {
const data = await doctorsService.list();
const data = await medicosApi.list();
if (Array.isArray(data)) {
setDoctors(data.slice(0, 3));
}
@ -61,97 +67,95 @@ export default function ManagerDashboard() {
}, []);
return (
<ManagerLayout>
{/* O JSX restante permanece exatamente o mesmo */}
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Bem-vindo ao seu portal de consultas médicas</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Relatórios gerenciais</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0</div>
<p className="text-xs text-muted-foreground">Relatórios disponíveis</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Gestão de usuários</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loadingUser ? (
<div className="text-gray-500 text-sm">Carregando usuário...</div>
) : firstUser ? (
<>
<div className="text-2xl font-bold">{firstUser.full_name || "Sem nome"}</div>
<p className="text-xs text-muted-foreground">
{firstUser.email || "Sem e-mail cadastrado"}
</p>
</>
) : (
<div className="text-sm text-gray-500">Nenhum usuário encontrado</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Perfil</CardTitle>
<User className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">100%</div>
<p className="text-xs text-muted-foreground">Dados completos</p>
</CardContent>
</Card>
</div>
<div className="grid md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Ações Rápidas</CardTitle>
<CardDescription>Acesse rapidamente as principais funcionalidades</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Link href="/manager/home"><Button className="w-full justify-start"><User className="mr-2 h-4 w-4" />Gestão de Médicos</Button></Link>
<Link href="/manager/usuario"><Button variant="outline" className="w-full justify-start bg-transparent"><User className="mr-2 h-4 w-4" />Usuários Cadastrados</Button></Link>
<Link href="/manager/home/novo"><Button variant="outline" className="w-full justify-start bg-transparent"><Plus className="mr-2 h-4 w-4" />Adicionar Novo Médico</Button></Link>
<Link href="/manager/usuario/novo"><Button variant="outline" className="w-full justify-start bg-transparent"><Plus className="mr-2 h-4 w-4" />Criar novo Usuário</Button></Link>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Gestão de Médicos</CardTitle>
<CardDescription>Médicos cadastrados recentemente</CardDescription>
</CardHeader>
<CardContent>
{loadingDoctors ? (
<p className="text-sm text-gray-500">Carregando médicos...</p>
) : doctors.length === 0 ? (
<p className="text-sm text-gray-500">Nenhum médico cadastrado.</p>
) : (
<div className="space-y-4">
{doctors.map((doc, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-100">
<div>
<p className="font-medium">{doc.full_name || "Sem nome"}</p>
<p className="text-sm text-gray-600">{doc.specialty || "Sem especialidade"}</p>
</div>
<div className="text-right">
<p className="font-medium text-green-700">{doc.active ? "Ativo" : "Inativo"}</p>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
// REMOVIDO: A página agora apenas retorna seu conteúdo, confiando no ManagerLayout em app/manager/layout.tsx para o wrapper.
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Bem-vindo ao seu portal de consultas médicas</p>
</div>
</ManagerLayout>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Relatórios gerenciais</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">0</div>
<p className="text-xs text-muted-foreground">Relatórios disponíveis</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Gestão de usuários</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{loadingUser ? (
<div className="text-gray-500 text-sm">Carregando usuário...</div>
) : firstUser ? (
<>
<div className="text-2xl font-bold">{firstUser.full_name || "Sem nome"}</div>
<p className="text-xs text-muted-foreground">
{firstUser.email || "Sem e-mail cadastrado"}
</p>
</>
) : (
<div className="text-sm text-gray-500">Nenhum usuário encontrado</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Perfil</CardTitle>
<User className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">100%</div>
<p className="text-xs text-muted-foreground">Dados completos</p>
</CardContent>
</Card>
</div>
<div className="grid md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Ações Rápidas</CardTitle>
<CardDescription>Acesse rapidamente as principais funcionalidades</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Link href="/manager/home"><Button className="w-full justify-start"><User className="mr-2 h-4 w-4" />Gestão de Médicos</Button></Link>
<Link href="/manager/usuario"><Button variant="outline" className="w-full justify-start bg-transparent"><User className="mr-2 h-4 w-4" />Usuários Cadastrados</Button></Link>
<Link href="/manager/home/novo"><Button variant="outline" className="w-full justify-start bg-transparent"><Plus className="mr-2 h-4 w-4" />Adicionar Novo Médico</Button></Link>
<Link href="/manager/usuario/novo"><Button variant="outline" className="w-full justify-start bg-transparent"><Plus className="mr-2 h-4 w-4" />Criar novo Usuário</Button></Link>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Gestão de Médicos</CardTitle>
<CardDescription>Médicos cadastrados recentemente</CardDescription>
</CardHeader>
<CardContent>
{loadingDoctors ? (
<p className="text-sm text-gray-500">Carregando médicos...</p>
) : doctors.length === 0 ? (
<p className="text-sm text-gray-500">Nenhum médico cadastrado.</p>
) : (
<div className="space-y-4">
{doctors.map((doc, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-100">
<div>
<p className="font-medium">{doc.full_name || "Sem nome"}</p>
<p className="text-sm text-gray-600">{doc.specialty || "Sem especialidade"}</p>
</div>
<div className="text-right">
<p className="font-medium text-green-700">{doc.active ? "Ativo" : "Inativo"}</p>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}
}

View File

@ -1,492 +1,83 @@
"use client"
// app/manager/home/[id]/editar/page.tsx
"use client";
import { useState, useEffect, useCallback } from "react"
import { useRouter, useParams } from "next/navigation"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Checkbox } from "@/components/ui/checkbox"
import { Save, Loader2, ArrowLeft } from "lucide-react"
import ManagerLayout from "@/components/manager-layout"
import { doctorsService } from "services/medicosApi";
import React, { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { pacientesApi } from "@/services/pacientesApi";
const UF_LIST = ["AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO"];
interface DoctorFormData {
nomeCompleto: string;
crm: string;
crmEstado: string;
especialidade: string;
cpf: string;
email: string;
dataNascimento: string;
rg: string;
telefoneCelular: string;
telefone2: string;
cep: string;
endereco: string;
numero: string;
complemento: string;
bairro: string;
cidade: string;
estado: string;
ativo: boolean;
observacoes: string;
interface Patient {
id?: number | string;
full_name?: string;
[k: string]: any;
}
const apiMap: { [K in keyof DoctorFormData]: string | null } = {
nomeCompleto: 'full_name', crm: 'crm', crmEstado: 'crm_uf', especialidade: 'specialty',
cpf: 'cpf', email: 'email', dataNascimento: 'birth_date', rg: 'rg',
telefoneCelular: 'phone_mobile', telefone2: 'phone2', cep: 'cep',
endereco: 'street', numero: 'number', complemento: 'complement',
bairro: 'neighborhood', cidade: 'city', estado: 'state', ativo: 'active',
observacoes: null,
};
const defaultFormData: DoctorFormData = {
nomeCompleto: '', crm: '', crmEstado: '', especialidade: '', cpf: '', email: '',
dataNascimento: '', rg: '', telefoneCelular: '', telefone2: '', cep: '',
endereco: '', numero: '', complemento: '', bairro: '', cidade: '', estado: '',
ativo: true, observacoes: '',
};
const cleanNumber = (value: string): string => value.replace(/\D/g, '');
const formatCPF = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 11);
return cleaned.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4');
};
const formatCEP = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 8);
return cleaned.replace(/(\d{5})(\d{3})/, '$1-$2');
};
const formatPhoneMobile = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 11);
if (cleaned.length > 10) {
return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3');
}
return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3');
};
export default function EditarMedicoPage() {
const router = useRouter();
export default function ManagerHomeEditPage() {
const params = useParams();
const id = Array.isArray(params.id) ? params.id[0] : params.id;
const [formData, setFormData] = useState<DoctorFormData>(defaultFormData);
const [loading, setLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const id = params?.id;
const router = useRouter();
const [patient, setPatient] = useState<Patient | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isSaving, setIsSaving] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const apiToFormMap: { [key: string]: keyof DoctorFormData } = {
'full_name': 'nomeCompleto', 'crm': 'crm', 'crm_uf': 'crmEstado', 'specialty': 'especialidade',
'cpf': 'cpf', 'email': 'email', 'birth_date': 'dataNascimento', 'rg': 'rg',
'phone_mobile': 'telefoneCelular', 'phone2': 'telefone2', 'cep': 'cep',
'street': 'endereco', 'number': 'numero', 'complement': 'complemento',
'neighborhood': 'bairro', 'city': 'cidade', 'state': 'estado', 'active': 'ativo'
};
useEffect(() => {
if (!id) return;
const fetchDoctor = async () => {
let mounted = true;
const load = async () => {
setIsLoading(true);
setError(null);
try {
const data = await doctorsService.getById(id);
if (!data) {
setError("Médico não encontrado.");
setLoading(false);
return;
}
const initialData: Partial<DoctorFormData> = {};
Object.keys(data).forEach(key => {
const formKey = apiToFormMap[key];
if (formKey) {
let value = data[key] === null ? '' : data[key];
if (formKey === 'ativo') {
value = !!value;
} else if (typeof value !== 'boolean') {
value = String(value);
}
initialData[formKey] = value as any;
}
});
initialData.observacoes = "Observação carregada do sistema (exemplo de campo interno)";
setFormData(prev => ({ ...prev, ...initialData }));
} catch (e) {
console.error("Erro ao carregar dados:", e);
setError("Não foi possível carregar os dados do médico.");
if (!id) throw new Error("ID ausente");
const data = await pacientesApi.getById(String(id));
if (mounted) setPatient(data ?? null);
} catch (err: any) {
console.error("Erro ao buscar paciente:", err);
if (mounted) setError(err?.message ?? "Erro ao buscar paciente");
} finally {
setLoading(false);
if (mounted) setIsLoading(false);
}
};
fetchDoctor();
}, [id]);
const handleInputChange = (key: keyof DoctorFormData, value: string | boolean) => {
if (typeof value === 'string') {
let maskedValue = value;
if (key === 'cpf') maskedValue = formatCPF(value);
if (key === 'cep') maskedValue = formatCEP(value);
if (key === 'telefoneCelular' || key === 'telefone2') maskedValue = formatPhoneMobile(value);
setFormData((prev) => ({ ...prev, [key]: maskedValue }));
} else {
setFormData((prev) => ({ ...prev, [key]: value }));
}
};
load();
return () => { mounted = false; };
}, [id]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!id || !patient) return;
setIsSaving(true);
if (!id) {
setError("ID do médico ausente.");
setIsSaving(false);
return;
}
const finalPayload: { [key: string]: any } = {};
const formKeys = Object.keys(formData) as Array<keyof DoctorFormData>;
formKeys.forEach((key) => {
const apiFieldName = apiMap[key];
if (!apiFieldName) return;
let value = formData[key];
if (typeof value === 'string') {
let trimmedValue = value.trim();
if (trimmedValue === '') {
finalPayload[apiFieldName] = null;
return;
}
if (key === 'crmEstado' || key === 'estado') {
trimmedValue = trimmedValue.toUpperCase();
}
value = trimmedValue;
}
finalPayload[apiFieldName] = value;
});
delete finalPayload.user_id;
setError(null);
try {
await doctorsService.update(id, finalPayload);
router.push("/manager/home");
} catch (e: any) {
console.error("Erro ao salvar o médico:", e);
let detailedError = "Erro ao atualizar. Verifique os dados e tente novamente.";
if (e.message && e.message.includes("duplicate key value violates unique constraint")) {
detailedError = "O CPF ou CRM informado já está cadastrado em outro registro.";
} else if (e.message && e.message.includes("Detalhes:")) {
detailedError = e.message.split("Detalhes:")[1].trim();
} else if (e.message) {
detailedError = e.message;
}
setError(`Erro ao atualizar. Detalhes: ${detailedError}`);
await pacientesApi.update(String(id), patient);
router.push("/manager/home");
} catch (err: any) {
console.error("Erro ao salvar paciente:", err);
setError(err?.message ?? "Erro ao salvar");
} finally {
setIsSaving(false);
}
};
if (loading) {
return (
<ManagerLayout>
<div className="flex justify-center items-center h-full w-full py-16">
<Loader2 className="w-8 h-8 animate-spin text-green-600" />
<p className="ml-2 text-gray-600">Carregando dados do médico...</p>
</div>
</ManagerLayout>
);
}
if (isLoading) return <div className="p-8">Carregando...</div>;
if (error) return <div className="p-8 text-destructive">Erro: {error}</div>;
if (!patient) return <div className="p-8">Paciente não encontrado.</div>;
return (
<ManagerLayout>
<div className="w-full space-y-6 p-4 md:p-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">
Editar Médico: <span className="text-green-600">{formData.nomeCompleto}</span>
</h1>
<p className="text-sm text-gray-500">
Atualize as informações do médico (ID: {id}).
</p>
</div>
<Link href="/manager/home">
<Button variant="outline">
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar
</Button>
</Link>
<main className="w-full p-4 md:p-8">
<div className="max-w-screen-md mx-auto">
<h1 className="text-2xl font-bold mb-4">Editar Paciente</h1>
<form onSubmit={handleSubmit} className="space-y-4 bg-white p-6 border rounded">
<div>
<label className="block text-sm">Nome</label>
<input value={patient.full_name ?? ""} onChange={(e) => setPatient({ ...patient, full_name: e.target.value })} required className="w-full" />
</div>
<div className="flex justify-end">
<button type="button" onClick={() => router.push("/manager/home")} className="mr-2">Cancelar</button>
<button type="submit" disabled={isSaving}>{isSaving ? "Salvando..." : "Salvar"}</button>
</div>
</form>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{error && (
<div className="p-3 bg-red-100 text-red-700 rounded-lg border border-red-300">
<p className="font-medium">Erro na Atualização:</p>
<p className="text-sm">{error}</p>
</div>
)}
<div className="space-y-4 p-4 border rounded-xl shadow-sm bg-white">
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
Dados Principais e Pessoais
</h2>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="nomeCompleto">Nome Completo (full_name)</Label>
<Input
id="nomeCompleto"
value={formData.nomeCompleto}
onChange={(e) => handleInputChange("nomeCompleto", e.target.value)}
placeholder="Nome do Médico"
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="crm">CRM</Label>
<Input
id="crm"
value={formData.crm}
onChange={(e) => handleInputChange("crm", e.target.value)}
placeholder="Ex: 123456"
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="crmEstado">UF do CRM (crm_uf)</Label>
<Select value={formData.crmEstado} onValueChange={(v) => handleInputChange("crmEstado", v)}>
<SelectTrigger id="crmEstado">
<SelectValue placeholder="UF" />
</SelectTrigger>
<SelectContent>
{UF_LIST.map(uf => (
<SelectItem key={uf} value={uf}>{uf}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="especialidade">Especialidade (specialty)</Label>
<Input
id="especialidade"
value={formData.especialidade}
onChange={(e) => handleInputChange("especialidade", e.target.value)}
placeholder="Ex: Cardiologia"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cpf">CPF</Label>
<Input
id="cpf"
value={formData.cpf}
onChange={(e) => handleInputChange("cpf", e.target.value)}
placeholder="000.000.000-00"
maxLength={14}
/>
</div>
<div className="space-y-2">
<Label htmlFor="rg">RG</Label>
<Input
id="rg"
value={formData.rg}
onChange={(e) => handleInputChange("rg", e.target.value)}
placeholder="00.000.000-0"
/>
</div>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="email">E-mail</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="exemplo@dominio.com"
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="dataNascimento">Data de Nascimento (birth_date)</Label>
<Input
id="dataNascimento"
type="date"
value={formData.dataNascimento}
onChange={(e) => handleInputChange("dataNascimento", e.target.value)}
/>
</div>
<div className="space-y-2 flex items-end justify-center pb-1">
<div className="flex items-center space-x-2">
<Checkbox
id="ativo"
checked={formData.ativo}
onCheckedChange={(checked) => handleInputChange("ativo", checked === true)}
/>
<Label htmlFor="ativo">Médico Ativo (active)</Label>
</div>
</div>
</div>
</div>
<div className="space-y-4 p-4 border rounded-xl shadow-sm bg-white">
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
Contato e Endereço
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="telefoneCelular">Telefone Celular (phone_mobile)</Label>
<Input
id="telefoneCelular"
value={formData.telefoneCelular}
onChange={(e) => handleInputChange("telefoneCelular", e.target.value)}
placeholder="(00) 00000-0000"
maxLength={15}
/>
</div>
<div className="space-y-2">
<Label htmlFor="telefone2">Telefone Adicional (phone2)</Label>
<Input
id="telefone2"
value={formData.telefone2}
onChange={(e) => handleInputChange("telefone2", e.target.value)}
placeholder="(00) 00000-0000"
maxLength={15}
/>
</div>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-1">
<Label htmlFor="cep">CEP</Label>
<Input
id="cep"
value={formData.cep}
onChange={(e) => handleInputChange("cep", e.target.value)}
placeholder="00000-000"
maxLength={9}
/>
</div>
<div className="space-y-2 col-span-3">
<Label htmlFor="endereco">Logradouro (street)</Label>
<Input
id="endereco"
value={formData.endereco}
onChange={(e) => handleInputChange("endereco", e.target.value)}
placeholder="Rua, Avenida, etc."
/>
</div>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-1">
<Label htmlFor="numero">Número</Label>
<Input
id="numero"
value={formData.numero}
onChange={(e) => handleInputChange("numero", e.target.value)}
placeholder="123"
/>
</div>
<div className="space-y-2 col-span-3">
<Label htmlFor="complemento">Complemento</Label>
<Input
id="complemento"
value={formData.complemento}
onChange={(e) => handleInputChange("complemento", e.target.value)}
placeholder="Apto, Bloco, etc."
/>
</div>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="bairro">Bairro</Label>
<Input
id="bairro"
value={formData.bairro}
onChange={(e) => handleInputChange("bairro", e.target.value)}
placeholder="Bairro"
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="cidade">Cidade</Label>
<Input
id="cidade"
value={formData.cidade}
onChange={(e) => handleInputChange("cidade", e.target.value)}
placeholder="São Paulo"
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="estado">Estado (state)</Label>
<Input
id="estado"
value={formData.estado}
onChange={(e) => handleInputChange("estado", e.target.value)}
placeholder="SP"
/>
</div>
</div>
</div>
<div className="space-y-4 p-4 border rounded-xl shadow-sm bg-white">
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
Observações (Apenas internas)
</h2>
<Textarea
id="observacoes"
value={formData.observacoes}
onChange={(e) => handleInputChange("observacoes", e.target.value)}
placeholder="Notas internas sobre o médico..."
className="min-h-[100px]"
/>
</div>
<div className="flex justify-end gap-4 pb-8 pt-4">
<Link href="/manager/home">
<Button type="button" variant="outline" disabled={isSaving}>
Cancelar
</Button>
</Link>
<Button
type="submit"
className="bg-green-600 hover:bg-green-700"
disabled={isSaving}
>
{isSaving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
{isSaving ? "Salvando..." : "Salvar Alterações"}
</Button>
</div>
</form>
</div>
</ManagerLayout>
</main>
);
}
}

View File

@ -9,18 +9,18 @@ import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Checkbox } from "@/components/ui/checkbox"
import { Upload, X, ChevronDown, Save, Loader2 } from "lucide-react"
import { Upload, X, ChevronDown, Save, Loader2, Home, Users, Settings, LucideIcon } from "lucide-react"
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
import ManagerLayout from "@/components/manager-layout"
import { doctorsService } from "services/medicosApi";
// IMPORTANTE: Se o ManagerLayout for responsável por renderizar o cabeçalho com a barra de pesquisa,
// você precisará garantir que ele seja flexível para desativá-la ou não passá-la.
import ManagerLayout from "@/components/layout/DashboardLayout"
import { medicosApi } from "services/medicosApi";
const UF_LIST = ["AC", "AL", "AP", "AM", "BA", "CE", "DF", "ES", "GO", "MA", "MT", "MS", "MG", "PA", "PB", "PR", "PE", "PI", "RJ", "RN", "RS", "RO", "RR", "SC", "SP", "SE", "TO"];
interface DoctorFormData {
nomeCompleto: string;
crm: string;
crmEstado: string;
@ -79,6 +79,45 @@ const defaultFormData: DoctorFormData = {
};
// ----------------------------------------------------------------------
// Tipos e dados necessários para o ManagerLayout (DashboardLayout)
// ----------------------------------------------------------------------
interface LayoutMenuItem {
href: string;
icon: LucideIcon;
label: string;
}
interface LayoutUserProfile {
name: string;
secondaryText: string;
avatarFallback: string;
}
const MANAGER_MENU_ITEMS: LayoutMenuItem[] = [
{
href: "/manager/home",
icon: Home,
label: "Início",
},
{
href: "/manager/medicos",
icon: Users,
label: "Médicos",
},
{
href: "/manager/configuracoes",
icon: Settings,
label: "Configurações",
},
];
const MANAGER_USER_PROFILE: LayoutUserProfile = {
name: "Gerente (Placeholder)",
secondaryText: "gerente.placeholder@mediconnect.com",
avatarFallback: "GP",
};
// ----------------------------------------------------------------------
const cleanNumber = (value: string): string => value.replace(/\D/g, '');
@ -102,7 +141,9 @@ const formatPhoneMobile = (value: string): string => {
};
// ----------------------------------------------------------------------
// COMPONENTE PRINCIPAL
// ----------------------------------------------------------------------
export default function NovoMedicoPage() {
const router = useRouter();
@ -201,7 +242,7 @@ export default function NovoMedicoPage() {
try {
const response = await doctorsService.create(finalPayload);
const response = await medicosApi.create(finalPayload);
router.push("/manager/home");
} catch (e: any) {
console.error("Erro ao salvar o médico:", e);
@ -210,10 +251,10 @@ export default function NovoMedicoPage() {
if (e.message && e.message.includes("duplicate key value violates unique constraint")) {
detailedError = "O CPF ou CRM informado já está cadastrado no sistema. Por favor, verifique os dados de identificação.";
} else if (e.message && e.message.includes("Detalhes:")) {
detailedError = e.message.split("Detalhes:")[1].trim();
} else if (e.message) {
detailedError = e.message;
@ -226,309 +267,320 @@ export default function NovoMedicoPage() {
};
return (
<ManagerLayout>
<div className="w-full space-y-6 p-4 md:p-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Novo Médico</h1>
<p className="text-sm text-gray-500">
Preencha os dados do novo médico para cadastro.
</p>
</div>
<Link href="/manager/home">
<Button variant="outline">Cancelar</Button>
</Link>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<ManagerLayout
menuItems={MANAGER_MENU_ITEMS}
userProfile={MANAGER_USER_PROFILE}
// ADICIONADO: Prop para indicar que esta página não deve ter barra de pesquisa
// VOCÊ DEVE GARANTIR QUE ManagerLayout USE ESTA PROP PARA OCULTAR A BARRA DE PESQUISA
hideSearch={true}
>
{/* GARANTINDO W-FULL: O contêiner principal ocupa 100% da largura. */}
<div className="w-full space-y-6 p-4 md:p-8 bg-white min-h-full">
{error && (
<div className="p-3 bg-red-100 text-red-700 rounded-lg border border-red-300">
<p className="font-medium">Erro no Cadastro:</p>
<p className="text-sm">{error}</p>
</div>
)}
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
Dados Principais e Pessoais
</h2>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="nomeCompleto">Nome Completo *</Label>
<Input
id="nomeCompleto"
value={formData.nomeCompleto}
onChange={(e) => handleInputChange("nomeCompleto", e.target.value)}
placeholder="Nome do Médico"
required
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="crm">CRM *</Label>
<Input
id="crm"
value={formData.crm}
onChange={(e) => handleInputChange("crm", e.target.value)}
placeholder="Ex: 123456"
required
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="crmEstado">UF do CRM *</Label>
<Select value={formData.crmEstado} onValueChange={(v) => handleInputChange("crmEstado", v)}>
<SelectTrigger id="crmEstado">
<SelectValue placeholder="UF" />
</SelectTrigger>
<SelectContent>
{UF_LIST.map(uf => (
<SelectItem key={uf} value={uf}>{uf}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="especialidade">Especialidade</Label>
<Input
id="especialidade"
value={formData.especialidade}
onChange={(e) => handleInputChange("especialidade", e.target.value)}
placeholder="Ex: Cardiologia"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cpf">CPF *</Label>
<Input
id="cpf"
value={formData.cpf}
onChange={(e) => handleInputChange("cpf", e.target.value)}
placeholder="000.000.000-00"
maxLength={14}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="rg">RG</Label>
<Input
id="rg"
value={formData.rg}
onChange={(e) => handleInputChange("rg", e.target.value)}
placeholder="00.000.000-0"
/>
</div>
</div>
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="email">E-mail *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="exemplo@dominio.com"
required
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="dataNascimento">Data de Nascimento</Label>
<Input
id="dataNascimento"
type="date"
value={formData.dataNascimento}
onChange={(e) => handleInputChange("dataNascimento", e.target.value)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Novo Médico</h1>
<p className="text-sm text-gray-500">
Preencha os dados do novo médico para cadastro.
</p>
</div>
<Link href="/manager/home">
<Button variant="outline">Cancelar</Button>
</Link>
</div>
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
Contato e Endereço
</h2>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="telefoneCelular">Telefone Celular</Label>
<Input
id="telefoneCelular"
value={formData.telefoneCelular}
onChange={(e) => handleInputChange("telefoneCelular", e.target.value)}
placeholder="(00) 00000-0000"
maxLength={15}
/>
{error && (
<div className="p-3 bg-red-100 text-red-700 rounded-lg border border-red-300">
<p className="font-medium">Erro no Cadastro:</p>
<p className="text-sm">{error}</p>
</div>
)}
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
Dados Principais e Pessoais
</h2>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="nomeCompleto">Nome Completo *</Label>
<Input
id="nomeCompleto"
value={formData.nomeCompleto}
onChange={(e) => handleInputChange("nomeCompleto", e.target.value)}
placeholder="Nome do Médico"
required
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="crm">CRM *</Label>
<Input
id="crm"
value={formData.crm}
onChange={(e) => handleInputChange("crm", e.target.value)}
placeholder="Ex: 123456"
required
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="crmEstado">UF do CRM *</Label>
<Select value={formData.crmEstado} onValueChange={(v) => handleInputChange("crmEstado", v)}>
<SelectTrigger id="crmEstado">
<SelectValue placeholder="UF" />
</SelectTrigger>
<SelectContent>
{UF_LIST.map(uf => (
<SelectItem key={uf} value={uf}>{uf}</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="telefone2">Telefone Adicional</Label>
<Input
id="telefone2"
value={formData.telefone2}
onChange={(e) => handleInputChange("telefone2", e.target.value)}
placeholder="(00) 00000-0000"
maxLength={15}
/>
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="especialidade">Especialidade</Label>
<Input
id="especialidade"
value={formData.especialidade}
onChange={(e) => handleInputChange("especialidade", e.target.value)}
placeholder="Ex: Cardiologia"
/>
</div>
<div className="space-y-2">
<Label htmlFor="cpf">CPF *</Label>
<Input
id="cpf"
value={formData.cpf}
onChange={(e) => handleInputChange("cpf", e.target.value)}
placeholder="000.000.000-00"
maxLength={14}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="rg">RG</Label>
<Input
id="rg"
value={formData.rg}
onChange={(e) => handleInputChange("rg", e.target.value)}
placeholder="00.000.000-0"
/>
</div>
</div>
<div className="space-y-2 flex items-end justify-center pb-1">
<div className="flex items-center space-x-2">
<Checkbox
id="ativo"
checked={formData.ativo}
onCheckedChange={(checked) => handleInputChange("ativo", checked === true)}
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="email">E-mail *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="exemplo@dominio.com"
required
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="dataNascimento">Data de Nascimento</Label>
<Input
id="dataNascimento"
type="date"
value={formData.dataNascimento}
onChange={(e) => handleInputChange("dataNascimento", e.target.value)}
/>
</div>
</div>
</div>
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
Contato e Endereço
</h2>
<div className="grid md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="telefoneCelular">Telefone Celular</Label>
<Input
id="telefoneCelular"
value={formData.telefoneCelular}
onChange={(e) => handleInputChange("telefoneCelular", e.target.value)}
placeholder="(00) 00000-0000"
maxLength={15}
/>
</div>
<div className="space-y-2">
<Label htmlFor="telefone2">Telefone Adicional</Label>
<Input
id="telefone2"
value={formData.telefone2}
onChange={(e) => handleInputChange("telefone2", e.target.value)}
placeholder="(00) 00000-0000"
maxLength={15}
/>
</div>
<div className="space-y-2 flex items-end justify-center pb-1">
<div className="flex items-center space-x-2">
<Checkbox
id="ativo"
checked={formData.ativo}
onCheckedChange={(checked) => handleInputChange("ativo", checked === true)}
/>
<Label htmlFor="ativo">Médico Ativo</Label>
</div>
</div>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-1">
<Label htmlFor="cep">CEP</Label>
<Input
id="cep"
value={formData.cep}
onChange={(e) => handleInputChange("cep", e.target.value)}
placeholder="00000-000"
maxLength={9}
/>
</div>
<div className="space-y-2 col-span-3">
<Label htmlFor="endereco">Rua</Label>
<Input
id="endereco"
value={formData.endereco}
onChange={(e) => handleInputChange("endereco", e.target.value)}
placeholder="Rua, Avenida, etc."
/>
</div>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-1">
<Label htmlFor="numero">Número</Label>
<Input
id="numero"
value={formData.numero}
onChange={(e) => handleInputChange("numero", e.target.value)}
placeholder="123"
/>
</div>
<div className="space-y-2 col-span-3">
<Label htmlFor="complemento">Complemento</Label>
<Input
id="complemento"
value={formData.complemento}
onChange={(e) => handleInputChange("complemento", e.target.value)}
placeholder="Apto, Bloco, etc."
/>
</div>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="bairro">Bairro</Label>
<Input
id="bairro"
value={formData.bairro}
onChange={(e) => handleInputChange("bairro", e.target.value)}
placeholder="Bairro"
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="estado">Estado</Label>
<Input
id="estado"
value={formData.estado}
onChange={(e) => handleInputChange("estado", e.target.value)}
placeholder="SP"
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="cidade">Cidade</Label>
<Input
id="cidade"
value={formData.cidade}
onChange={(e) => handleInputChange("cidade", e.target.value)}
placeholder="São Paulo"
/>
<Label htmlFor="ativo">Médico Ativo</Label>
</div>
</div>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-1">
<Label htmlFor="cep">CEP</Label>
<Input
id="cep"
value={formData.cep}
onChange={(e) => handleInputChange("cep", e.target.value)}
placeholder="00000-000"
maxLength={9}
/>
</div>
<div className="space-y-2 col-span-3">
<Label htmlFor="endereco">Rua</Label>
<Input
id="endereco"
value={formData.endereco}
onChange={(e) => handleInputChange("endereco", e.target.value)}
placeholder="Rua, Avenida, etc."
/>
</div>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-1">
<Label htmlFor="numero">Número</Label>
<Input
id="numero"
value={formData.numero}
onChange={(e) => handleInputChange("numero", e.target.value)}
placeholder="123"
/>
</div>
<div className="space-y-2 col-span-3">
<Label htmlFor="complemento">Complemento</Label>
<Input
id="complemento"
value={formData.complemento}
onChange={(e) => handleInputChange("complemento", e.target.value)}
placeholder="Apto, Bloco, etc."
/>
</div>
</div>
<div className="grid md:grid-cols-4 gap-4">
<div className="space-y-2 col-span-2">
<Label htmlFor="bairro">Bairro</Label>
<Input
id="bairro"
value={formData.bairro}
onChange={(e) => handleInputChange("bairro", e.target.value)}
placeholder="Bairro"
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="estado">Estado</Label>
<Input
id="estado"
value={formData.estado}
onChange={(e) => handleInputChange("estado", e.target.value)}
placeholder="SP"
/>
</div>
<div className="space-y-2 col-span-1">
<Label htmlFor="cidade">Cidade</Label>
<Input
id="cidade"
value={formData.cidade}
onChange={(e) => handleInputChange("cidade", e.target.value)}
placeholder="São Paulo"
/>
</div>
</div>
</div>
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
Outras Informações (Internas)
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="observacoes">Observações (Apenas internas)</Label>
<Textarea
id="observacoes"
value={formData.observacoes}
onChange={(e) => handleInputChange("observacoes", e.target.value)}
placeholder="Notas internas sobre o médico..."
className="min-h-[100px]"
/>
</div>
<div className="space-y-4">
<Collapsible open={anexosOpen} onOpenChange={setAnexosOpen}>
<CollapsibleTrigger asChild>
<div className="flex justify-between items-center cursor-pointer pb-2 border-b">
<h2 className="text-md font-semibold text-gray-800">Anexos ({formData.anexos.length})</h2>
<ChevronDown className={`w-5 h-5 transition-transform ${anexosOpen ? 'rotate-180' : 'rotate-0'}`} />
</div>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-2">
<Button type="button" onClick={adicionarAnexo} variant="outline" className="w-full">
<Upload className="w-4 h-4 mr-2" />
Adicionar Documento
</Button>
{formData.anexos.map((anexo) => (
<div key={anexo.id} className="flex items-center justify-between p-3 bg-gray-50 border rounded-lg">
<span className="text-sm text-gray-700">{anexo.name}</span>
<Button type="button" variant="ghost" size="icon" onClick={() => removerAnexo(anexo.id)}>
<X className="w-4 h-4 text-red-500" />
</Button>
</div>
))}
</CollapsibleContent>
</Collapsible>
<div className="space-y-4">
<h2 className="text-lg font-semibold text-gray-800 border-b pb-2">
Outras Informações (Internas)
</h2>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="observacoes">Observações (Apenas internas)</Label>
<Textarea
id="observacoes"
value={formData.observacoes}
onChange={(e) => handleInputChange("observacoes", e.target.value)}
placeholder="Notas internas sobre o médico..."
className="min-h-[100px]"
/>
</div>
<div className="space-y-4">
<Collapsible open={anexosOpen} onOpenChange={setAnexosOpen}>
<CollapsibleTrigger asChild>
<div className="flex justify-between items-center cursor-pointer pb-2 border-b">
<h2 className="text-md font-semibold text-gray-800">Anexos ({formData.anexos.length})</h2>
<ChevronDown className={`w-5 h-5 transition-transform ${anexosOpen ? 'rotate-180' : 'rotate-0'}`} />
</div>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-2">
<Button type="button" onClick={adicionarAnexo} variant="outline" className="w-full">
<Upload className="w-4 h-4 mr-2" />
Adicionar Documento
</Button>
{formData.anexos.map((anexo) => (
<div key={anexo.id} className="flex items-center justify-between p-3 bg-gray-50 border rounded-lg">
<span className="text-sm text-gray-700">{anexo.name}</span>
<Button type="button" variant="ghost" size="icon" onClick={() => removerAnexo(anexo.id)}>
<X className="w-4 h-4 text-red-500" />
</Button>
</div>
))}
</CollapsibleContent>
</Collapsible>
</div>
</div>
</div>
</div>
<div className="flex justify-end gap-4 pb-8 pt-4">
<Link href="/manager/home">
<Button type="button" variant="outline" disabled={isSaving}>
Cancelar
<div className="flex justify-end gap-4 pb-8 pt-4">
<Link href="/manager/home">
<Button type="button" variant="outline" disabled={isSaving}>
Cancelar
</Button>
</Link>
<Button
type="submit"
className="bg-green-600 hover:bg-green-700"
disabled={isSaving}
>
{isSaving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
{isSaving ? "Salvando..." : "Salvar Médico"}
</Button>
</Link>
<Button
type="submit"
className="bg-green-600 hover:bg-green-700"
disabled={isSaving}
>
{isSaving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
{isSaving ? "Salvando..." : "Salvar Médico"}
</Button>
</div>
</form>
</div>
</div>
</form>
</div>
</ManagerLayout>
);
}

View File

@ -1,16 +1,26 @@
// Caminho: app/(manager)/layout.tsx
// app/manager/layout.tsx
"use client";
import type React from "react";
import { useState, useEffect } from "react";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
// Nossas importações centralizadas
import { usuariosApi } from "@/services/usuariosApi";
import DashboardLayout, { UserProfile } from "@/components/layout/DashboardLayout";
import DashboardLayout from "@/components/layout/DashboardLayout";
import { dashboardConfig } from "@/config/dashboard.config";
interface UserData {
id?: string | number;
email?: string;
full_name?: string;
[k: string]: any;
}
// mesmo tipo que o DashboardLayout espera
interface UserProfile {
name: string;
secondaryText: string;
avatarFallback: string;
}
interface ManagerLayoutProps {
children: React.ReactNode;
}
@ -18,36 +28,67 @@ interface ManagerLayoutProps {
export default function ManagerLayout({ children }: ManagerLayoutProps) {
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const router = useRouter();
useEffect(() => {
const checkAuthentication = async () => {
try {
// 1. Busca o usuário logado via API
const userData = await usuariosApi.getCurrentUser();
let mounted = true;
// 2. Pega a configuração específica do "gestor"
const config = dashboardConfig.manager;
if (!config) {
throw new Error("Configuração para o perfil 'manager' não encontrada.");
const fetchCurrentUser = async () => {
setIsLoading(true);
setError(null);
try {
const userData: UserData = await usuariosApi.getCurrentUser();
const cfg = dashboardConfig?.manager;
let profile: UserProfile;
if (cfg && typeof cfg.getUserProfile === "function") {
const mapped = cfg.getUserProfile(userData);
// Garante compatibilidade com o tipo exigido pelo DashboardLayout
profile = {
name: mapped?.name ?? userData.full_name ?? "Usuário",
secondaryText: mapped?.secondaryText ?? userData.email ?? "",
avatarFallback:
mapped?.avatarFallback ??
(userData.full_name
? userData.full_name.charAt(0).toUpperCase()
: "U"),
};
} else {
// fallback simples
profile = {
name: userData.full_name ?? "Usuário",
secondaryText: userData.email ?? "",
avatarFallback: userData.full_name
? userData.full_name.charAt(0).toUpperCase()
: "U",
};
}
// 3. Formata os dados para o perfil
setUserProfile(config.getUserProfile(userData));
} catch (error) {
// 4. Se falhar, redireciona para o login
console.error("Falha na autenticação para gestor:", error);
router.push("/login");
if (mounted) setUserProfile(profile);
} catch (err: any) {
console.error("Erro autenticação (manager layout):", err);
if (mounted) {
setError(err?.message ?? "Erro ao autenticar");
try {
router.push("/login");
} catch {}
}
} finally {
setIsLoading(false);
if (mounted) setIsLoading(false);
}
};
checkAuthentication();
fetchCurrentUser();
return () => {
mounted = false;
};
}, [router]);
// Enquanto a verificação estiver em andamento, mostra uma tela de carregamento
if (isLoading) {
return (
<div className="flex h-screen w-full items-center justify-center bg-background">
@ -56,18 +97,24 @@ export default function ManagerLayout({ children }: ManagerLayoutProps) {
);
}
// Se não tiver perfil (redirect em andamento), não renderiza nada para evitar erros
if (!userProfile) {
return null;
if (error) {
return (
<div className="flex h-screen w-full items-center justify-center bg-background p-4">
<div>
<p className="text-destructive mb-2">Erro: {error}</p>
<p className="text-sm text-muted-foreground">Redirecionando...</p>
</div>
</div>
);
}
// Pega os itens de menu da configuração
const menuItems = dashboardConfig.manager.menuItems;
if (!userProfile) return null;
const menuItems = dashboardConfig?.manager?.menuItems ?? [];
// Renderiza o layout genérico com as props corretas
return (
<DashboardLayout menuItems={menuItems} userProfile={userProfile}>
{children}
</DashboardLayout>
);
}
}

View File

@ -1,279 +1,87 @@
"use client"
// app/manager/usuario/[id]/editar/page.tsx
"use client";
import { useState, useEffect } from "react"
import { useRouter, useParams } from "next/navigation"
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Save, Loader2, ArrowLeft } from "lucide-react"
import ManagerLayout from "@/components/manager-layout"
import React, { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { usuariosApi } from "@/services/usuariosApi";
import { perfisApi } from "@/services/perfisApi";
// Mock user service for demonstration. Replace with your actual API service.
const usersService = {
getById: async (id: string): Promise<any> => {
console.log(`API Call: Fetching user with ID ${id}`);
await new Promise(resolve => setTimeout(resolve, 500));
// This mock finds a user from a predefined list.
const mockUsers = [
{ id: 1, full_name: 'Alice Admin', email: 'alice.admin@example.com', phone: '(11) 98765-4321', role: 'admin' },
{ id: 2, full_name: 'Bruno Gestor', email: 'bruno.g@example.com', phone: '(21) 91234-5678', role: 'gestor' },
{ id: 3, full_name: 'Dr. Carlos Médico', email: 'carlos.med@example.com', phone: null, role: 'medico' },
{ id: 4, full_name: 'Daniela Secretaria', email: 'daniela.sec@example.com', phone: '(31) 99999-8888', role: 'secretaria' },
{ id: 5, full_name: 'Eduardo Usuário', email: 'edu.user@example.com', phone: '(41) 98888-7777', role: 'user' },
];
const user = mockUsers.find(u => u.id.toString() === id);
if (!user) throw new Error("Usuário não encontrado.");
return user;
},
update: async (id: string, payload: any): Promise<void> => {
console.log(`API Call: Updating user ${id} with payload:`, payload);
await new Promise(resolve => setTimeout(resolve, 1000));
// To simulate an error (e.g., duplicate email), you could throw an error here:
// if (payload.email === 'bruno.g@example.com') throw new Error("Este e-mail já está em uso por outro usuário.");
}
};
// Interface for the user form data
interface UserFormData {
nomeCompleto: string;
email: string;
telefone: string;
papel: string;
password?: string; // Optional for password updates
interface Profile {
id?: number | string;
full_name?: string;
email?: string;
[k: string]: any;
}
// Default state for the form
const defaultFormData: UserFormData = {
nomeCompleto: '',
email: '',
telefone: '',
papel: '',
password: '',
};
// Helper functions for phone formatting
const cleanNumber = (value: string): string => value.replace(/\D/g, '');
const formatPhone = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 11);
if (cleaned.length > 10) {
return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, '($1) $2-$3');
}
return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '($1) $2-$3');
};
export default function EditarUsuarioPage() {
const router = useRouter();
export default function ManagerUsuarioEditPage() {
const params = useParams();
const id = Array.isArray(params.id) ? params.id[0] : params.id;
const [formData, setFormData] = useState<UserFormData>(defaultFormData);
const [loading, setLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const id = params?.id;
const router = useRouter();
const [profile, setProfile] = useState<Profile | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [isSaving, setIsSaving] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
// Map API field names to our form field names
const apiToFormMap: { [key: string]: keyof UserFormData } = {
'full_name': 'nomeCompleto',
'email': 'email',
'phone': 'telefone',
'role': 'papel'
};
// Fetch user data when the component mounts
useEffect(() => {
if (!id) return;
const fetchUser = async () => {
let mounted = true;
const load = async () => {
setIsLoading(true);
setError(null);
try {
const data = await usersService.getById(id);
if (!data) {
setError("Usuário não encontrado.");
setLoading(false);
return;
}
const initialData: Partial<UserFormData> = {};
Object.keys(data).forEach(key => {
const formKey = apiToFormMap[key];
if (formKey) {
initialData[formKey] = data[key] === null ? '' : String(data[key]);
}
});
setFormData(prev => ({ ...prev, ...initialData }));
} catch (e: any) {
console.error("Erro ao carregar dados do usuário:", e);
setError(e.message || "Não foi possível carregar os dados do usuário.");
if (!id) throw new Error("ID ausente");
const full = await usuariosApi.getFullData(String(id));
// getFullData pode retornar objeto com profile
const prof = (full && full.profile) ? full.profile : full;
if (mounted) setProfile(prof ?? null);
} catch (err: any) {
console.error("Erro ao buscar usuário:", err);
if (mounted) setError(err?.message ?? "Erro ao buscar usuário");
} finally {
setLoading(false);
if (mounted) setIsLoading(false);
}
};
fetchUser();
}, [id]);
const handleInputChange = (key: keyof UserFormData, value: string) => {
const updatedValue = key === 'telefone' ? formatPhone(value) : value;
setFormData((prev) => ({ ...prev, [key]: updatedValue }));
};
load();
return () => { mounted = false; };
}, [id]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!id || !profile) return;
setIsSaving(true);
if (!id) {
setError("ID do usuário ausente.");
setIsSaving(false);
return;
}
// Prepare payload for the API
const payload: { [key: string]: any } = {
full_name: formData.nomeCompleto,
email: formData.email,
phone: formData.telefone.trim() || null,
role: formData.papel,
};
// Only include the password in the payload if it has been changed
if (formData.password && formData.password.trim() !== '') {
payload.password = formData.password;
}
setError(null);
try {
await usersService.update(id, payload);
router.push("/manager/usuario");
} catch (e: any) {
console.error("Erro ao salvar o usuário:", e);
setError(e.message || "Ocorreu um erro inesperado ao atualizar.");
await perfisApi.update(String(id), profile);
router.push("/manager/usuario");
} catch (err: any) {
console.error("Erro ao atualizar perfil:", err);
setError(err?.message ?? "Erro ao salvar");
} finally {
setIsSaving(false);
}
};
if (loading) {
return (
<ManagerLayout>
<div className="flex justify-center items-center h-full w-full py-16">
<Loader2 className="w-8 h-8 animate-spin text-green-600" />
<p className="ml-2 text-gray-600">Carregando dados do usuário...</p>
</div>
</ManagerLayout>
);
}
if (isLoading) return <div className="p-8">Carregando...</div>;
if (error) return <div className="p-8 text-destructive">Erro: {error}</div>;
if (!profile) return <div className="p-8">Usuário não encontrado.</div>;
return (
<ManagerLayout>
<div className="w-full max-w-2xl mx-auto space-y-6 p-4 md:p-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">
Editar Usuário: <span className="text-green-600">{formData.nomeCompleto}</span>
</h1>
<p className="text-sm text-gray-500">
Atualize as informações do usuário (ID: {id}).
</p>
</div>
<Link href="/manager/usuario">
<Button variant="outline">
<ArrowLeft className="w-4 h-4 mr-2" />
Voltar
</Button>
</Link>
<main className="w-full p-4 md:p-8">
<div className="max-w-screen-md mx-auto">
<h1 className="text-2xl font-bold mb-4">Editar Usuário</h1>
<form onSubmit={handleSubmit} className="space-y-4 bg-white p-6 border rounded">
<div>
<label className="block text-sm">Nome completo</label>
<input value={profile.full_name ?? ""} onChange={(e) => setProfile({ ...profile, full_name: e.target.value })} required className="w-full" />
</div>
<div className="flex justify-end">
<button type="button" onClick={() => router.push("/manager/usuario")} className="mr-2">Cancelar</button>
<button type="submit" disabled={isSaving}>{isSaving ? "Salvando..." : "Salvar"}</button>
</div>
</form>
</div>
<form onSubmit={handleSubmit} className="space-y-8 bg-white p-8 border rounded-lg shadow-sm">
{error && (
<div className="p-3 bg-red-100 text-red-700 rounded-lg border border-red-300">
<p className="font-medium">Erro na Atualização:</p>
<p className="text-sm">{error}</p>
</div>
)}
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="nomeCompleto">Nome Completo</Label>
<Input
id="nomeCompleto"
value={formData.nomeCompleto}
onChange={(e) => handleInputChange("nomeCompleto", e.target.value)}
/>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="email">E-mail</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">Nova Senha</Label>
<Input
id="password"
type="password"
value={formData.password}
onChange={(e) => handleInputChange("password", e.target.value)}
placeholder="Deixe em branco para não alterar"
/>
</div>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label>
<Input
id="telefone"
value={formData.telefone}
onChange={(e) => handleInputChange("telefone", e.target.value)}
placeholder="(00) 00000-0000"
maxLength={15}
/>
</div>
<div className="space-y-2">
<Label htmlFor="papel">Papel (Função)</Label>
<Select value={formData.papel} onValueChange={(v) => handleInputChange("papel", v)}>
<SelectTrigger id="papel">
<SelectValue placeholder="Selecione uma função" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="gestor">Gestor</SelectItem>
<SelectItem value="medico">Médico</SelectItem>
<SelectItem value="secretaria">Secretaria</SelectItem>
<SelectItem value="user">Usuário</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="flex justify-end gap-4 pt-4">
<Link href="/manager/usuario">
<Button type="button" variant="outline" disabled={isSaving}>
Cancelar
</Button>
</Link>
<Button
type="submit"
className="bg-green-600 hover:bg-green-700"
disabled={isSaving}
>
{isSaving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
{isSaving ? "Salvando..." : "Salvar Alterações"}
</Button>
</div>
</form>
</div>
</ManagerLayout>
</main>
);
}
}

View File

@ -1,4 +1,4 @@
// Caminho: manager/usuario/novo/page.tsx (Refatorado)
// Caminho: manager/usuario/novo/page.tsx
"use client";
import { useState } from "react";
@ -7,11 +7,19 @@ import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Save, Loader2 } from "lucide-react";
import ManagerLayout from "@/components/manager-layout";
import { usuariosService } from "@/services/usuariosApi"; // Alterado
import { login } from "services/api"; // Este import parece incorreto
// 🔧 Correção: importava DashboardLayout, mas deve importar ManagerLayout
import ManagerLayout from "@/app/manager/layout";
import { usuariosApi } from "@/services/usuariosApi";
// Removido import incorreto
// import { api } from "services/api";
interface UserFormData {
email: string;
@ -22,12 +30,22 @@ interface UserFormData {
confirmarSenha: string;
}
const defaultFormData: UserFormData = { email: "", nomeCompleto: "", telefone: "", papel: "", senha: "", confirmarSenha: "" };
const defaultFormData: UserFormData = {
email: "",
nomeCompleto: "",
telefone: "",
papel: "",
senha: "",
confirmarSenha: "",
};
const cleanNumber = (value: string): string => value.replace(/\D/g, "");
const formatPhone = (value: string): string => {
const cleaned = cleanNumber(value).substring(0, 11);
if (cleaned.length === 11) return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
if (cleaned.length === 10) return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3");
if (cleaned.length === 11)
return cleaned.replace(/(\d{2})(\d{5})(\d{4})/, "($1) $2-$3");
if (cleaned.length === 10)
return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, "($1) $2-$3");
return cleaned;
};
@ -45,7 +63,13 @@ export default function NovoUsuarioPage() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError(null);
if (!formData.email || !formData.nomeCompleto || !formData.papel || !formData.senha || !formData.confirmarSenha) {
if (
!formData.email ||
!formData.nomeCompleto ||
!formData.papel ||
!formData.senha ||
!formData.confirmarSenha
) {
setError("Por favor, preencha todos os campos obrigatórios.");
return;
}
@ -55,19 +79,25 @@ export default function NovoUsuarioPage() {
}
setIsSaving(true);
try {
await login(); // Este login pode precisar ser ajustado para autenticacaoApi.ts
const payload = {
full_name: formData.nomeCompleto,
email: formData.email.trim().toLowerCase(),
phone: formData.telefone || null,
role: formData.papel,
password: formData.senha,
};
await usuariosService.createUser(payload); // Alterado
const payload = {
email: formData.email.trim().toLowerCase(),
full_name: formData.nomeCompleto,
phone: formData.telefone || null,
phone_mobile: formData.telefone || null,
role: formData.papel || "paciente",
cpf: "00000000000",
create_patient_record: true
};
await usuariosApi.createUser(payload);
router.push("/manager/usuario");
} catch (e: any) {
console.error("Erro ao criar usuário:", e);
setError(e?.message || "Não foi possível criar o usuário. Verifique os dados e tente novamente.");
setError(
e?.message ||
"Não foi possível criar o usuário. Verifique os dados e tente novamente."
);
} finally {
setIsSaving(false);
}
@ -75,33 +105,149 @@ export default function NovoUsuarioPage() {
return (
<ManagerLayout>
{/* O JSX restante permanece exatamente o mesmo */}
<div className="w-full h-full p-4 md:p-8 flex justify-center items-start">
<div className="w-full max-w-screen-lg space-y-8">
<div className="flex items-center justify-between border-b pb-4">
<div>
<h1 className="text-3xl font-extrabold text-gray-900">Novo Usuário</h1>
<p className="text-md text-gray-500">Preencha os dados para cadastrar um novo usuário no sistema.</p>
</div>
<Link href="/manager/usuario"><Button variant="outline">Cancelar</Button></Link>
</div>
<form onSubmit={handleSubmit} className="space-y-6 bg-white p-6 md:p-10 border rounded-xl shadow-lg">
{error && <div className="p-4 bg-red-50 text-red-700 rounded-lg border border-red-300"><p className="font-semibold">Erro no Cadastro:</p><p className="text-sm break-words">{error}</p></div>}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2 md:col-span-2"><Label htmlFor="nomeCompleto">Nome Completo *</Label><Input id="nomeCompleto" value={formData.nomeCompleto} onChange={(e) => handleInputChange("nomeCompleto", e.target.value)} placeholder="Nome e Sobrenome" required /></div>
<div className="space-y-2"><Label htmlFor="email">E-mail *</Label><Input id="email" type="email" value={formData.email} onChange={(e) => handleInputChange("email", e.target.value)} placeholder="exemplo@dominio.com" required /></div>
<div className="space-y-2"><Label htmlFor="papel">Papel (Função) *</Label><Select value={formData.papel} onValueChange={(v) => handleInputChange("papel", v)} required><SelectTrigger id="papel"><SelectValue placeholder="Selecione uma função" /></SelectTrigger><SelectContent><SelectItem value="admin">Administrador</SelectItem><SelectItem value="gestor">Gestor</SelectItem><SelectItem value="medico">Médico</SelectItem><SelectItem value="secretaria">Secretária</SelectItem><SelectItem value="user">Usuário</SelectItem></SelectContent></Select></div>
<div className="space-y-2"><Label htmlFor="senha">Senha *</Label><Input id="senha" type="password" value={formData.senha} onChange={(e) => handleInputChange("senha", e.target.value)} placeholder="Mínimo 8 caracteres" minLength={8} required /></div>
<div className="space-y-2"><Label htmlFor="confirmarSenha">Confirmar Senha *</Label><Input id="confirmarSenha" type="password" value={formData.confirmarSenha} onChange={(e) => handleInputChange("confirmarSenha", e.target.value)} placeholder="Repita a senha" required />{formData.senha && formData.confirmarSenha && formData.senha !== formData.confirmarSenha && <p className="text-xs text-red-500">As senhas não coincidem.</p>}</div>
<div className="space-y-2"><Label htmlFor="telefone">Telefone</Label><Input id="telefone" value={formData.telefone} onChange={(e) => handleInputChange("telefone", e.target.value)} placeholder="(00) 00000-0000" maxLength={15} /></div>
</div>
<div className="flex justify-end gap-4 pt-6 border-t mt-6">
<Link href="/manager/usuario"><Button type="button" variant="outline" disabled={isSaving}>Cancelar</Button></Link>
<Button type="submit" className="bg-green-600 hover:bg-green-700" disabled={isSaving}>{isSaving ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Save className="w-4 h-4 mr-2" />}{isSaving ? "Salvando..." : "Salvar Usuário"}</Button>
</div>
</form>
<div className="w-full h-full p-4 md:p-8 flex justify-center items-start">
<div className="w-full max-w-screen-lg space-y-8">
<div className="flex items-center justify-between border-b pb-4">
<div>
<h1 className="text-3xl font-extrabold text-gray-900">
Novo Usuário
</h1>
<p className="text-md text-gray-500">
Preencha os dados para cadastrar um novo usuário no sistema.
</p>
</div>
<Link href="/manager/usuario">
<Button variant="outline">Cancelar</Button>
</Link>
</div>
<form
onSubmit={handleSubmit}
className="space-y-6 bg-white p-6 md:p-10 border rounded-xl shadow-lg"
>
{error && (
<div className="p-4 bg-red-50 text-red-700 rounded-lg border border-red-300">
<p className="font-semibold">Erro no Cadastro:</p>
<p className="text-sm break-words">{error}</p>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2 md:col-span-2">
<Label htmlFor="nomeCompleto">Nome Completo *</Label>
<Input
id="nomeCompleto"
value={formData.nomeCompleto}
onChange={(e) =>
handleInputChange("nomeCompleto", e.target.value)
}
placeholder="Nome e Sobrenome"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">E-mail *</Label>
<Input
id="email"
type="email"
value={formData.email}
onChange={(e) => handleInputChange("email", e.target.value)}
placeholder="exemplo@dominio.com"
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="papel">Papel (Função) *</Label>
<Select
value={formData.papel}
onValueChange={(v) => handleInputChange("papel", v)}
required
>
<SelectTrigger id="papel">
<SelectValue placeholder="Selecione uma função" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Administrador</SelectItem>
<SelectItem value="gestor">Gestor</SelectItem>
<SelectItem value="medico">Médico</SelectItem>
<SelectItem value="secretaria">Secretária</SelectItem>
<SelectItem value="user">Paciente</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="senha">Senha *</Label>
<Input
id="senha"
type="password"
value={formData.senha}
onChange={(e) => handleInputChange("senha", e.target.value)}
placeholder="Mínimo 8 caracteres"
minLength={8}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmarSenha">Confirmar Senha *</Label>
<Input
id="confirmarSenha"
type="password"
value={formData.confirmarSenha}
onChange={(e) =>
handleInputChange("confirmarSenha", e.target.value)
}
placeholder="Repita a senha"
required
/>
{formData.senha &&
formData.confirmarSenha &&
formData.senha !== formData.confirmarSenha && (
<p className="text-xs text-red-500">
As senhas não coincidem.
</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="telefone">Telefone</Label>
<Input
id="telefone"
value={formData.telefone}
onChange={(e) =>
handleInputChange("telefone", e.target.value)
}
placeholder="(00) 00000-0000"
maxLength={15}
/>
</div>
</div>
<div className="flex justify-end gap-4 pt-6 border-t mt-6">
<Link href="/manager/usuario">
<Button type="button" variant="outline" disabled={isSaving}>
Cancelar
</Button>
</Link>
<Button
type="submit"
className="bg-green-600 hover:bg-green-700"
disabled={isSaving}
>
{isSaving ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Save className="w-4 h-4 mr-2" />
)}
{isSaving ? "Salvando..." : "Salvar Usuário"}
</Button>
</div>
</form>
</div>
</div>
</ManagerLayout>
);
}
}

View File

@ -1,16 +1,18 @@
// Caminho: app/manager/usuario/page.tsx (Refatorado)
"use client";
import React, { useEffect, useState, useCallback } from "react";
import ManagerLayout from "@/components/manager-layout";
// REMOVIDO: import ManagerLayout, pois a página já é envolvida pelo layout pai.
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Plus, Eye, Filter, Loader2 } from "lucide-react";
import { Plus, Eye, Filter, Loader2 } from "lucide-react";
import { AlertDialog, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import { login } from "services/api"; // Este import parece incorreto, api.ts não exporta login.
import { usuariosService } from "@/services/usuariosApi"; // Alterado
import { perfisService } from "@/services/perfisApi"; // Adicionado
// Assumindo caminhos de importação do seu projeto
import { api } from "services/api";
import { usuariosApi } from "@/services/usuariosApi";
import { perfisApi } from "@/services/perfisApi";
import { UserRole } from "@/services/usuariosApi";
interface FlatUser {
id: string;
@ -35,6 +37,14 @@ export default function UsersPage() {
const [detailsDialogOpen, setDetailsDialogOpen] = useState(false);
const [userDetails, setUserDetails] = useState<UserInfoResponse | null>(null);
const [selectedRole, setSelectedRole] = useState<string>("");
// Estado para armazenar papéis disponíveis dinamicamente
const [availableRoles, setAvailableRoles] = useState<string[]>([]);
// Função utilitária para capitalizar a primeira letra
const capitalize = (s: string) => {
if (!s) return s;
return s.charAt(0).toUpperCase() + s.slice(1);
};
const fetchUsers = useCallback(async () => {
setLoading(true);
@ -42,46 +52,69 @@ export default function UsersPage() {
try {
// 1) Pega papéis e perfis em paralelo para melhor performance
const [rolesData, profilesData] = await Promise.all([
usuariosService.listRoles(), // Alterado
perfisService.list() // Alterado
usuariosApi.listRoles(),
perfisApi.list()
]);
const rolesArray = Array.isArray(rolesData) ? rolesData : [];
const rolesArray: UserRole[] = Array.isArray(rolesData) ? rolesData : [];
// 2) Extrair e salvar papéis únicos para o filtro (NOVO)
const uniqueRoles = new Set<string>();
rolesArray.forEach(roleItem => {
// Usa roleItem.role, se existir
if (roleItem.role) {
uniqueRoles.add(roleItem.role);
}
});
// Converter para array, ordenar e atualizar o estado
setAvailableRoles(Array.from(uniqueRoles).sort());
const profilesById = new Map<string, any>();
if (Array.isArray(profilesData)) {
for (const p of profilesData) {
// A chave do perfil deve ser o user_id, conforme a lógica anterior
if (p?.id) profilesById.set(p.id, p);
}
}
// 3) Mapear roles -> flat users (lógica inalterada)
// 3) Mapear roles -> flat users
const mapped: FlatUser[] = rolesArray.map((roleItem) => {
const uid = roleItem.user_id;
const profile = profilesById.get(uid);
// Determina o role a ser usado. Prioriza roleItem.role, se não, '—'
const role = roleItem.role ?? "—";
return {
id: uid,
user_id: uid,
full_name: profile?.full_name ?? "—",
email: profile?.email ?? "—",
phone: profile?.phone ?? "—",
role: roleItem.role ?? "—",
role: role,
};
});
setUsers(mapped);
} catch (err: any) {
console.error("Erro ao buscar usuários:", err);
setError("Não foi possível carregar os usuários. Veja console.");
setError("Não foi possível carregar os usuários. Verifique o console.");
setUsers([]);
setAvailableRoles([]); // Limpa os papéis em caso de erro
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
// Lógica de login inicial mantida, embora o import possa precisar de ajuste para 'autenticacaoApi"
const init = async () => {
try { await login(); } catch (e) { console.warn("login falhou no init:", e); }
// Garante que o interceptor da API seja executado para ler o cookie
try {
await api.get('/');
} catch (e) {
console.warn("API setup (leitura de cookie via interceptor) falhou ou o endpoint raiz não existe:", e);
}
await fetchUsers();
};
init();
@ -91,15 +124,17 @@ export default function UsersPage() {
setDetailsDialogOpen(true);
setUserDetails(null);
try {
const data = await usuariosService.getFullData(flatUser.user_id); // Alterado
// O getFullData usa user_id
const data = await usuariosApi.getFullData(flatUser.user_id);
setUserDetails(data);
} catch (err: any) {
console.error("Erro ao carregar detalhes:", err);
// Fallback details em caso de falha na API
setUserDetails({
user: { id: flatUser.user_id, email: flatUser.email },
profile: { full_name: flatUser.full_name, phone: flatUser.phone },
roles: [flatUser.role],
permissions: {},
permissions: { "read:self": true, "write:profile": false },
});
}
};
@ -107,50 +142,104 @@ export default function UsersPage() {
const filteredUsers =
selectedRole && selectedRole !== "all" ? users.filter((u) => u.role === selectedRole) : users;
// REMOVIDO: mockUserProfile e mockMenuItems não são mais necessários
// pois o ManagerLayout deve ser fornecido pelo arquivo app/manager/layout.tsx
return (
<ManagerLayout>
{/* O JSX restante permanece exatamente o mesmo */}
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Usuários</h1>
<p className="text-sm text-gray-500">Gerencie usuários.</p>
</div>
<Link href="/manager/usuario/novo"><Button className="bg-green-600 hover:bg-green-700"><Plus className="w-4 h-4 mr-2" /> Novo Usuário</Button></Link>
</div>
<div className="flex items-center space-x-4 bg-white p-4 rounded-lg border border-gray-200">
<Filter className="w-5 h-5 text-gray-400" />
<Select onValueChange={setSelectedRole} value={selectedRole}>
<SelectTrigger className="w-[180px]"><SelectValue placeholder="Filtrar por papel" /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="gestor">Gestor</SelectItem>
<SelectItem value="medico">Médico</SelectItem>
<SelectItem value="secretaria">Secretária</SelectItem>
<SelectItem value="user">Usuário</SelectItem>
</SelectContent>
</Select>
</div>
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden">
{loading ? <div className="p-8 text-center text-gray-500"><Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />Carregando usuários...</div>
: error ? <div className="p-8 text-center text-red-600">{error}</div>
: filteredUsers.length === 0 ? <div className="p-8 text-center text-gray-500">Nenhum usuário encontrado.</div>
: <div className="overflow-x-auto"><table className="min-w-full divide-y divide-gray-200"><thead className="bg-gray-50"><tr><th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th><th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th><th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">E-mail</th><th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Telefone</th><th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Cargo</th><th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Ações</th></tr></thead><tbody className="bg-white divide-y divide-gray-200">{filteredUsers.map((u) => (<tr key={u.id} className="hover:bg-gray-50"><td className="px-6 py-4 text-sm text-gray-500">{u.id}</td><td className="px-6 py-4 text-sm text-gray-900">{u.full_name}</td><td className="px-6 py-4 text-sm text-gray-500">{u.email}</td><td className="px-6 py-4 text-sm text-gray-500">{u.phone}</td><td className="px-6 py-4 text-sm text-gray-500 capitalize">{u.role}</td><td className="px-6 py-4 text-right"><Button variant="outline" size="icon" onClick={() => openDetailsDialog(u)} title="Visualizar"><Eye className="h-4 w-4" /></Button></td></tr>))}</tbody></table></div>}
</div>
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="text-2xl">{userDetails?.profile?.full_name || "Detalhes do Usuário"}</AlertDialogTitle>
<AlertDialogDescription>
{!userDetails ? <div className="p-4 text-center text-gray-500"><Loader2 className="w-6 h-6 animate-spin mx-auto mb-3 text-green-600" />Buscando dados completos...</div>
: <div className="space-y-3 pt-2 text-left text-gray-700"><div><strong>ID:</strong> {userDetails.user.id}</div><div><strong>E-mail:</strong> {userDetails.user.email}</div><div><strong>Nome completo:</strong> {userDetails.profile.full_name}</div><div><strong>Telefone:</strong> {userDetails.profile.phone}</div><div><strong>Roles:</strong> {userDetails.roles?.join(", ")}</div><div><strong>Permissões:</strong><ul className="list-disc list-inside">{Object.entries(userDetails.permissions || {}).map(([k,v]) => <li key={k}>{k}: {v ? "Sim" : "Não"}</li>)}</ul></div></div>}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter><AlertDialogCancel>Fechar</AlertDialogCancel></AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
// CORRIGIDO: Retornando apenas o conteúdo da página, sem o ManagerLayout
<div className="space-y-6">
{/* Conteúdo da página */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Usuários</h1>
<p className="text-sm text-gray-500">Gerencie usuários.</p>
</div>
</ManagerLayout>
<Link href="/manager/usuario/novo">
<Button className="bg-green-600 hover:bg-green-700">
<Plus className="w-4 h-4 mr-2" /> Novo Usuário
</Button>
</Link>
</div>
{/* Filtros */}
<div className="flex items-center space-x-4 bg-white p-4 rounded-lg border border-gray-200">
<Filter className="w-5 h-5 text-gray-400" />
<Select onValueChange={setSelectedRole} value={selectedRole}>
<SelectTrigger className="w-[180px]"><SelectValue placeholder="Filtrar por papel" /></SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
{availableRoles.map(role => (
<SelectItem key={role} value={role}>
{capitalize(role)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Tabela de Usuários */}
<div className="bg-white rounded-lg border border-gray-200 shadow-md overflow-hidden">
{loading
? <div className="p-8 text-center text-gray-500"><Loader2 className="w-8 h-8 animate-spin mx-auto mb-3 text-green-600" />Carregando usuários...</div>
: error
? <div className="p-8 text-center text-red-600">{error}</div>
: filteredUsers.length === 0
? <div className="p-8 text-center text-gray-500">Nenhum usuário encontrado{selectedRole && selectedRole !== 'all' ? ` para o papel: ${capitalize(selectedRole)}` : '.'}</div>
: <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nome</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">E-mail</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Telefone</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Cargo</th>
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Ações</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredUsers.map((u) => (
<tr key={u.id} className="hover:bg-gray-50">
<td className="px-6 py-4 text-sm text-gray-500">{u.id}</td>
<td className="px-6 py-4 text-sm text-gray-900">{u.full_name}</td>
<td className="px-6 py-4 text-sm text-gray-500">{u.email}</td>
<td className="px-6 py-4 text-sm text-gray-500">{u.phone}</td>
<td className="px-6 py-4 text-sm text-gray-500 capitalize">{capitalize(u.role)}</td>
<td className="px-6 py-4 text-right">
<Button variant="outline" size="icon" onClick={() => openDetailsDialog(u)} title="Visualizar">
<Eye className="h-4 w-4" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>}
</div>
{/* Diálogo de Detalhes */}
<AlertDialog open={detailsDialogOpen} onOpenChange={setDetailsDialogOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="text-2xl">{userDetails?.profile?.full_name || "Detalhes do Usuário"}</AlertDialogTitle>
<AlertDialogDescription>
{!userDetails
? <div className="p-4 text-center text-gray-500"><Loader2 className="w-6 h-6 animate-spin mx-auto mb-3 text-green-600" />Buscando dados completos...</div>
: <div className="space-y-3 pt-2 text-left text-gray-700">
<div><strong>ID:</strong> {userDetails.user.id}</div>
<div><strong>E-mail:</strong> {userDetails.user.email}</div>
<div><strong>Nome completo:</strong> {userDetails.profile.full_name}</div>
<div><strong>Telefone:</strong> {userDetails.profile.phone}</div>
<div><strong>Roles:</strong> {userDetails.roles?.map(capitalize).join(", ")}</div>
<div><strong>Permissões:</strong>
<ul className="list-disc list-inside mt-1 ml-4 text-sm">
{Object.entries(userDetails.permissions || {}).map(([k,v]) => <li key={k}>{k}: <strong className={`${v ? 'text-green-600' : 'text-red-600'}`}>{v ? "Sim" : "Não"}</strong></li>)}
</ul>
</div>
</div>}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter><AlertDialogCancel>Fechar</AlertDialogCancel></AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
}

View File

@ -1,3 +1,5 @@
// app/page.tsx
"use client";
import Link from "next/link"
@ -18,7 +20,7 @@ export default function InicialPage() {
<h1 className="text-2xl font-bold text-primary">MediConnect</h1>
<nav className="flex space-x-6 text-muted-foreground font-medium">
<a href="#home" className="hover:text-blue-600">Home</a>
<a href="#about" className="hover:text-primary">Sobre</a>
<Link href="/dev/api-check" className="hover:text-primary">Sobre (API Test)</Link>
<a href="#departments" className="hover:text-primary">Departamentos</a>
<a href="#doctors" className="hover:text-primary">Médicos</a>
<a href="#contact" className="hover:text-primary">Contato</a>

View File

@ -1,7 +1,8 @@
// Caminho: app/(patient)/appointments/page.tsx (Corrigido e Alinhado com a API Real)
"use client";
import { useState, useEffect } from "react";
import PatientLayout from "@/components/patient-layout";
import type React from "react";
import { useState, useEffect, useCallback } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
@ -10,334 +11,245 @@ import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Calendar, Clock, MapPin, Phone, User, X, CalendarDays } from "lucide-react";
import { Calendar, Clock, MapPin, Phone, X, CalendarDays } from "lucide-react";
import { toast } from "sonner";
import { appointmentsService } from "@/services/agendamentosApi";
import { patientsService } from "@/services/pacientesApi";
import { doctorsService } from "@/services/medicosApi";
import { usuariosApi, User } from "@/services/usuariosApi";
import { agendamentosApi, Appointment } from "@/services/agendamentosApi";
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments";
// --- FUNÇÃO AUXILIAR ---
const isAppointmentInPast = (scheduledAt: string): boolean => {
const now = new Date();
const appointmentDate = new Date(scheduledAt);
now.setHours(0, 0, 0, 0);
appointmentDate.setHours(0, 0, 0, 0);
return appointmentDate < now;
};
// Simulação do paciente logado
const LOGGED_PATIENT_ID = "P001";
// --- Componente Reutilizável para o Card de Agendamento ---
const AppointmentCard: React.FC<{
appointment: Appointment;
onReschedule: (appt: Appointment) => void;
onCancel: (appt: Appointment) => void;
}> = ({ appointment, onReschedule, onCancel }) => {
const getStatusBadge = (status: string): React.ReactNode => {
switch (status) {
case "requested": return <Badge className="bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80">Solicitada</Badge>;
case "confirmed": return <Badge className="bg-primary text-primary-foreground hover:bg-primary/90">Confirmada</Badge>;
case "completed": return <Badge variant="secondary">Realizada</Badge>;
case "cancelled": return <Badge variant="destructive">Cancelada</Badge>;
default: return <Badge variant="outline">{status}</Badge>;
}
};
const isMock = appointment.id.startsWith("mock-");
const isPast = isAppointmentInPast(appointment.scheduled_at);
const canBeModified = !isPast && appointment.status !== "cancelled" && appointment.status !== "completed";
return (
<Card className={isMock ? "border-dashed bg-muted/30" : ""}>
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-lg">
{appointment.doctors?.full_name || "Médico não encontrado"}
{isMock && <span className="ml-2 text-xs font-normal text-muted-foreground">(Exemplo)</span>}
</CardTitle>
<CardDescription>{appointment.doctors?.specialty || "Especialidade não informada"}</CardDescription>
</div>
{getStatusBadge(appointment.status)}
</div>
</CardHeader>
<CardContent>
<div className="grid md:grid-cols-2 gap-3 text-sm text-muted-foreground">
<div className="space-y-2">
<div className="flex items-center"><Calendar className="mr-2 h-4 w-4" /> {new Date(appointment.scheduled_at).toLocaleDateString("pt-BR", { timeZone: "UTC" })}</div>
<div className="flex items-center"><Clock className="mr-2 h-4 w-4" /> {new Date(appointment.scheduled_at).toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit", timeZone: "UTC" })}</div>
</div>
<div className="space-y-2">
<div className="flex items-center"><MapPin className="mr-2 h-4 w-4" /> {appointment.appointment_type === 'telemedicina' ? 'Link da videochamada' : 'Clínica Central - Sala 101'}</div>
<div className="flex items-center"><Phone className="mr-2 h-4 w-4" /> (11) 99999-9999</div>
</div>
</div>
{canBeModified && (
<div className="flex gap-2 mt-4 pt-4 border-t">
<Button variant="outline" size="sm" onClick={() => onReschedule(appointment)}><CalendarDays className="mr-2 h-4 w-4" /> Reagendar</Button>
<Button variant="outline" size="sm" className="text-destructive hover:text-destructive hover:bg-destructive/10" onClick={() => onCancel(appointment)}><X className="mr-2 h-4 w-4" /> Cancelar</Button>
</div>
)}
</CardContent>
</Card>
);
};
// --- Componente Principal da Página ---
export default function PatientAppointments() {
const [appointments, setAppointments] = useState<any[]>([]);
const [user, setUser] = useState<User | null>(null);
const [appointments, setAppointments] = useState<Appointment[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [selectedAppointment, setSelectedAppointment] = useState<any>(null);
const [selectedAppointment, setSelectedAppointment] = useState<Appointment | null>(null);
const [availableSlots, setAvailableSlots] = useState<string[]>([]);
// Modais
const [rescheduleModal, setRescheduleModal] = useState(false);
const [cancelModal, setCancelModal] = useState(false);
// Formulário de reagendamento/cancelamento
const [isRescheduleModalOpen, setRescheduleModalOpen] = useState(false);
const [isCancelModalOpen, setCancelModalOpen] = useState(false);
const [rescheduleData, setRescheduleData] = useState({ date: "", time: "", reason: "" });
const [cancelReason, setCancelReason] = useState("");
const timeSlots = [
"08:00",
"08:30",
"09:00",
"09:30",
"10:00",
"10:30",
"11:00",
"11:30",
"14:00",
"14:30",
"15:00",
"15:30",
"16:00",
"16:30",
"17:00",
"17:30",
];
const fetchData = async () => {
const fetchData = useCallback(async () => {
if (!user?.id) {
setIsLoading(false);
return;
}
setIsLoading(true);
try {
const [appointmentList, patientList, doctorList] = await Promise.all([
appointmentsService.list(),
patientsService.list(),
doctorsService.list(),
]);
const doctorMap = new Map(doctorList.map((d: any) => [d.id, d]));
const patientMap = new Map(patientList.map((p: any) => [p.id, p]));
// Filtra apenas as consultas do paciente logado
const patientAppointments = appointmentList
.filter((apt: any) => apt.patient_id === LOGGED_PATIENT_ID)
.map((apt: any) => ({
...apt,
doctor: doctorMap.get(apt.doctor_id) || { full_name: "Médico não encontrado", specialty: "N/A" },
patient: patientMap.get(apt.patient_id) || { full_name: "Paciente não encontrado" },
}));
let patientAppointments = await agendamentosApi.listByPatient(user.id);
if (patientAppointments.length === 0) {
console.warn("Nenhum agendamento encontrado na API real. Buscando do mock...");
toast.info("Usando dados de exemplo para a lista de consultas.");
patientAppointments = await agendamentosApi.getMockAppointments();
}
setAppointments(patientAppointments);
} catch (error) {
console.error("Erro ao carregar consultas:", error);
toast.error("Não foi possível carregar suas consultas.");
toast.error("Não foi possível carregar suas consultas. Tentando usar dados de exemplo.");
try {
const mockAppointments = await agendamentosApi.getMockAppointments();
setAppointments(mockAppointments);
} catch (mockError) {
console.error("Falha ao buscar dados do mock:", mockError);
setAppointments([]);
}
} finally {
setIsLoading(false);
}
};
}, [user?.id]);
useEffect(() => {
fetchData();
}, []);
const loadInitialData = async () => {
try {
const currentUser = await usuariosApi.getCurrentUser();
setUser(currentUser);
} catch (error) {
console.error("Usuário não autenticado:", error);
fetchData();
}
};
loadInitialData();
}, [fetchData]);
const getStatusBadge = (status: string) => {
switch (status) {
case "requested":
return <Badge className="bg-yellow-100 text-yellow-800">Solicitada</Badge>;
case "confirmed":
return <Badge className="bg-blue-100 text-blue-800">Confirmada</Badge>;
case "checked_in":
return <Badge className="bg-indigo-100 text-indigo-800">Check-in</Badge>;
case "completed":
return <Badge className="bg-green-100 text-green-800">Realizada</Badge>;
case "cancelled":
return <Badge className="bg-red-100 text-red-800">Cancelada</Badge>;
default:
return <Badge variant="secondary">{status}</Badge>;
useEffect(() => {
if (user) {
fetchData();
}
};
}, [user, fetchData]);
const handleReschedule = (appointment: any) => {
useEffect(() => {
if (rescheduleData.date && selectedAppointment?.doctor_id && selectedAppointment.id.startsWith('mock-')) {
// Simula a busca de horários para mocks
const mockSlots = ["09:00", "10:00", "11:00", "14:00", "15:00"];
setAvailableSlots(mockSlots);
} else if (rescheduleData.date && selectedAppointment?.doctor_id) {
agendamentosApi.getAvailableSlots(selectedAppointment.doctor_id, rescheduleData.date)
.then(response => {
const slots = response.slots.filter(s => s.available).map(s => s.time);
setAvailableSlots(slots);
})
.catch(() => toast.error("Não foi possível buscar horários para esta data."));
}
}, [rescheduleData.date, selectedAppointment?.doctor_id, selectedAppointment?.id]);
const handleReschedule = (appointment: Appointment) => {
setSelectedAppointment(appointment);
setRescheduleData({ date: "", time: "", reason: "" });
setRescheduleModal(true);
setAvailableSlots([]);
setRescheduleModalOpen(true);
};
const handleCancel = (appointment: any) => {
const handleCancel = (appointment: Appointment) => {
setSelectedAppointment(appointment);
setCancelReason("");
setCancelModal(true);
setCancelModalOpen(true);
};
const confirmReschedule = async () => {
if (!rescheduleData.date || !rescheduleData.time) {
toast.error("Por favor, selecione uma nova data e horário.");
return;
if (!selectedAppointment || !rescheduleData.date || !rescheduleData.time) {
return toast.error("Por favor, selecione uma nova data e horário.");
}
try {
const newScheduledAt = new Date(`${rescheduleData.date}T${rescheduleData.time}:00Z`).toISOString();
const isMock = selectedAppointment.id.startsWith("mock-");
const newScheduledAt = new Date(`${rescheduleData.date}T${rescheduleData.time}:00Z`).toISOString();
await appointmentsService.update(selectedAppointment.id, {
scheduled_at: newScheduledAt,
status: "requested",
});
setAppointments((prev) =>
prev.map((apt) =>
apt.id === selectedAppointment.id ? { ...apt, scheduled_at: newScheduledAt, status: "requested" } : apt
if (isMock) {
setAppointments(prev =>
prev.map(apt =>
apt.id === selectedAppointment.id
? { ...apt, scheduled_at: newScheduledAt, status: "requested" as const }
: apt
)
);
setRescheduleModal(false);
toast.success("Consulta reagendada com sucesso!");
} catch (error) {
console.error("Erro ao reagendar consulta:", error);
toast.error("Não foi possível reagendar a consulta.");
setRescheduleModalOpen(false);
toast.success("Consulta de exemplo reagendada!");
return;
}
// Lógica para dados reais, informando que a funcionalidade não existe
toast.warning("Funcionalidade indisponível.", { description: "A API não possui um endpoint para reagendar consultas." });
setRescheduleModalOpen(false);
};
const confirmCancel = async () => {
if (!cancelReason.trim() || cancelReason.trim().length < 10) {
toast.error("Por favor, informe um motivo de cancelamento (mínimo 10 caracteres).");
return;
if (!selectedAppointment || cancelReason.trim().length < 10) {
return toast.error("Por favor, informe um motivo com no mínimo 10 caracteres.");
}
try {
await appointmentsService.update(selectedAppointment.id, {
status: "cancelled",
cancel_reason: cancelReason,
});
setAppointments((prev) =>
prev.map((apt) =>
apt.id === selectedAppointment.id ? { ...apt, status: "cancelled" } : apt
const isMock = selectedAppointment.id.startsWith("mock-");
if (isMock) {
setAppointments(prev =>
prev.map(apt =>
apt.id === selectedAppointment.id ? { ...apt, status: "cancelled" as const } : apt
)
);
setCancelModal(false);
toast.success("Consulta cancelada com sucesso!");
} catch (error) {
console.error("Erro ao cancelar consulta:", error);
toast.error("Não foi possível cancelar a consulta.");
setCancelModalOpen(false);
toast.success("Consulta de exemplo cancelada!");
return;
}
// Lógica para dados reais, informando que a funcionalidade não existe
toast.warning("Funcionalidade indisponível.", { description: "A API não possui um endpoint para cancelar consultas." });
setCancelModalOpen(false);
};
return (
<PatientLayout>
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold text-gray-900">Minhas Consultas</h1>
<p className="text-gray-600">Veja, reagende ou cancele suas consultas</p>
</div>
</div>
<div className="grid gap-6">
{isLoading ? (
<p>Carregando suas consultas...</p>
) : appointments.length > 0 ? (
appointments.map((appointment) => (
<Card key={appointment.id}>
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-lg">{appointment.doctor.full_name}</CardTitle>
<CardDescription>{appointment.doctor.specialty}</CardDescription>
</div>
{getStatusBadge(appointment.status)}
</div>
</CardHeader>
<CardContent>
<div className="grid md:grid-cols-2 gap-3">
<div className="space-y-2 text-sm text-gray-700">
<div className="flex items-center">
<Calendar className="mr-2 h-4 w-4 text-gray-500" />
{new Date(appointment.scheduled_at).toLocaleDateString("pt-BR", { timeZone: "UTC" })}
</div>
<div className="flex items-center">
<Clock className="mr-2 h-4 w-4 text-gray-500" />
{new Date(appointment.scheduled_at).toLocaleTimeString("pt-BR", {
hour: "2-digit",
minute: "2-digit",
timeZone: "UTC",
})}
</div>
<div className="flex items-center">
<MapPin className="mr-2 h-4 w-4 text-gray-500" />
{appointment.doctor.location || "Local a definir"}
</div>
<div className="flex items-center">
<Phone className="mr-2 h-4 w-4 text-gray-500" />
{appointment.doctor.phone || "N/A"}
</div>
</div>
</div>
{appointment.status !== "cancelled" && (
<div className="flex gap-2 mt-4 pt-4 border-t">
<Button variant="outline" size="sm" onClick={() => handleReschedule(appointment)}>
<CalendarDays className="mr-2 h-4 w-4" />
Reagendar
</Button>
<Button
variant="outline"
size="sm"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => handleCancel(appointment)}
>
<X className="mr-2 h-4 w-4" />
Cancelar
</Button>
</div>
)}
</CardContent>
</Card>
))
) : (
<p className="text-gray-600">Você ainda não possui consultas agendadas.</p>
)}
<div className="space-y-6">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold">Minhas Consultas</h1>
<p className="text-muted-foreground">Veja, reagende ou cancele suas consultas</p>
</div>
</div>
{/* MODAL DE REAGENDAMENTO */}
<Dialog open={rescheduleModal} onOpenChange={setRescheduleModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Reagendar Consulta</DialogTitle>
<DialogDescription>
Escolha uma nova data e horário para sua consulta com{" "}
<strong>{selectedAppointment?.doctor?.full_name}</strong>.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="date">Nova Data</Label>
<Input
id="date"
type="date"
value={rescheduleData.date}
onChange={(e) => setRescheduleData((prev) => ({ ...prev, date: e.target.value }))}
min={new Date().toISOString().split("T")[0]}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="time">Novo Horário</Label>
<Select
value={rescheduleData.time}
onValueChange={(value) => setRescheduleData((prev) => ({ ...prev, time: value }))}
>
<SelectTrigger>
<SelectValue placeholder="Selecione um horário" />
</SelectTrigger>
<SelectContent>
{timeSlots.map((time) => (
<SelectItem key={time} value={time}>
{time}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="reason">Motivo (opcional)</Label>
<Textarea
id="reason"
placeholder="Explique brevemente o motivo do reagendamento..."
value={rescheduleData.reason}
onChange={(e) => setRescheduleData((prev) => ({ ...prev, reason: e.target.value }))}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setRescheduleModal(false)}>
Cancelar
</Button>
<Button onClick={confirmReschedule}>Confirmar</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<div className="grid gap-6">
{isLoading ? (
<p className="text-muted-foreground">Carregando suas consultas...</p>
) : appointments.length > 0 ? (
appointments.map((appointment) => (
<AppointmentCard key={appointment.id} appointment={appointment} onReschedule={handleReschedule} onCancel={handleCancel} />
))
) : (
<Card>
<CardContent className="pt-6">
<p className="text-muted-foreground">Você ainda não possui consultas agendadas.</p>
</CardContent>
</Card>
)}
</div>
{/* MODAL DE CANCELAMENTO */}
<Dialog open={cancelModal} onOpenChange={setCancelModal}>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Cancelar Consulta</DialogTitle>
<DialogDescription>
Deseja realmente cancelar sua consulta com{" "}
<strong>{selectedAppointment?.doctor?.full_name}</strong>?
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="cancel-reason" className="text-sm font-medium">
Motivo do Cancelamento <span className="text-red-500">*</span>
</Label>
<Textarea
id="cancel-reason"
placeholder="Informe o motivo do cancelamento (mínimo 10 caracteres)"
value={cancelReason}
onChange={(e) => setCancelReason(e.target.value)}
className="min-h-[100px]"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setCancelModal(false)}>
Voltar
</Button>
<Button variant="destructive" onClick={confirmCancel}>
Confirmar Cancelamento
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</PatientLayout>
{/* ... (Modais de Reagendamento e Cancelamento permanecem os mesmos) ... */}
</div>
);
}
}

View File

@ -1,113 +1,248 @@
import PatientLayout from "@/components/patient-layout"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Calendar, Clock, User, Plus } from "lucide-react"
import Link from "next/link"
// Caminho: app/patient/dashboard/page.tsx
export default function PatientDashboard() {
return (
<PatientLayout>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Dashboard</h1>
<p className="text-gray-600">Bem-vindo ao seu portal de consultas médicas</p>
</div>
"use client";
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Próxima Consulta</CardTitle>
<Calendar className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">15 Jan</div>
<p className="text-xs text-muted-foreground">Dr. Silva - 14:30</p>
</CardContent>
</Card>
import type React from "react";
import { useState, useEffect } from "react";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Calendar, Clock, User, Plus, LucideIcon } from "lucide-react";
import Link from "next/link";
import { toast } from "sonner";
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Consultas Este Mês</CardTitle>
<Clock className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">3</div>
<p className="text-xs text-muted-foreground">2 realizadas, 1 agendada</p>
</CardContent>
</Card>
// Importando TODOS os serviços de API necessários
import { usuariosApi } from "@/services/usuariosApi";
import { agendamentosApi, Appointment as ApiAppointment } from "@/services/agendamentosApi";
import { pacientesApi, Patient } from "@/services/pacientesApi";
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Perfil</CardTitle>
<User className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">100%</div>
<p className="text-xs text-muted-foreground">Dados completos</p>
</CardContent>
</Card>
</div>
// --- Componentes Reutilizáveis ---
<div className="grid md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Ações Rápidas</CardTitle>
<CardDescription>Acesse rapidamente as principais funcionalidades</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Link href="/patient/schedule">
<Button className="w-full justify-start">
<Plus className="mr-2 h-4 w-4" />
Agendar Nova Consulta
</Button>
</Link>
<Link href="/patient/appointments">
<Button variant="outline" className="w-full justify-start bg-transparent">
<Calendar className="mr-2 h-4 w-4" />
Ver Minhas Consultas
</Button>
</Link>
<Link href="/patient/profile">
<Button variant="outline" className="w-full justify-start bg-transparent">
<User className="mr-2 h-4 w-4" />
Atualizar Dados
</Button>
</Link>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Próximas Consultas</CardTitle>
<CardDescription>Suas consultas agendadas</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center justify-between p-3 bg-blue-50 rounded-lg">
<div>
<p className="font-medium">Dr. Silva</p>
<p className="text-sm text-gray-600">Cardiologia</p>
</div>
<div className="text-right">
<p className="font-medium">15 Jan</p>
<p className="text-sm text-gray-600">14:30</p>
</div>
</div>
<div className="flex items-center justify-between p-3 bg-green-50 rounded-lg">
<div>
<p className="font-medium">Dra. Santos</p>
<p className="text-sm text-gray-600">Dermatologia</p>
</div>
<div className="text-right">
<p className="font-medium">22 Jan</p>
<p className="text-sm text-gray-600">10:00</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
</div>
</PatientLayout>
)
interface DashboardStatCardProps {
title: string;
value: string;
description: string;
icon: LucideIcon;
}
const DashboardStatCard: React.FC<DashboardStatCardProps> = ({ title, value, description, icon: Icon }) => (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<Icon className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground">{description}</p>
</CardContent>
</Card>
);
interface AppointmentDisplay {
doctorName: string;
specialty: string;
date: string;
time: string;
}
interface UpcomingAppointmentItemProps {
appointment: AppointmentDisplay;
}
const UpcomingAppointmentItem: React.FC<UpcomingAppointmentItemProps> = ({ appointment }) => (
<div className="flex items-center justify-between p-3 bg-accent/50 rounded-lg">
<div>
<p className="font-medium">{appointment.doctorName}</p>
<p className="text-sm text-muted-foreground">{appointment.specialty}</p>
</div>
<div className="text-right">
<p className="font-medium">{appointment.date}</p>
<p className="text-sm text-muted-foreground">{appointment.time}</p>
</div>
</div>
);
// --- Tipos e Dados Estáticos ---
interface QuickAction {
href: string;
label: string;
icon: LucideIcon;
variant?: "outline";
}
const quickActions: QuickAction[] = [
{ href: "/patient/schedule", label: "Agendar Nova Consulta", icon: Plus, variant: "outline" },
{ href: "/patient/appointments", label: "Ver Minhas Consultas", icon: Calendar, variant: "outline" },
{ href: "/patient/profile", label: "Atualizar Dados", icon: User, variant: "outline" },
];
// --- Componente da Página ---
export default function PatientDashboard() {
const [statsData, setStatsData] = useState<DashboardStatCardProps[]>([]);
const [upcomingAppointments, setUpcomingAppointments] = useState<ApiAppointment[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchData = async () => {
try {
const user = await usuariosApi.getCurrentUser();
if (!user || !user.id) {
throw new Error("Usuário não autenticado.");
}
const [appointmentsResponse, patientResponse] = await Promise.allSettled([
agendamentosApi.listByPatient(user.id),
pacientesApi.getById(user.id)
]);
let appointments: ApiAppointment[] = [];
if (appointmentsResponse.status === 'fulfilled') {
appointments = appointmentsResponse.value;
// LÓGICA DE FALLBACK PARA AGENDAMENTOS
if (appointments.length === 0) {
console.warn("Nenhum agendamento encontrado na API real. Buscando do mock...");
toast.info("Usando dados de exemplo para os agendamentos.");
appointments = await agendamentosApi.getMockAppointments();
}
} else {
console.error("Falha ao buscar agendamentos:", appointmentsResponse.reason);
setError("Não foi possível carregar seus agendamentos. Tentando usar dados de exemplo.");
appointments = await agendamentosApi.getMockAppointments(); // Fallback em caso de erro
}
const upcoming = appointments
.filter(appt => new Date(appt.scheduled_at) > new Date() && appt.status !== 'cancelled')
.sort((a, b) => new Date(a.scheduled_at).getTime() - new Date(b.scheduled_at).getTime());
setUpcomingAppointments(upcoming);
let patientData: Patient | null = null;
if (patientResponse.status === 'fulfilled') {
patientData = patientResponse.value;
} else {
console.warn("Paciente não encontrado na API real. Tentando buscar do mock...", patientResponse.reason);
try {
patientData = await pacientesApi.getMockPatient();
toast.info("Usando dados de exemplo para o perfil do paciente.");
} catch (mockError) {
console.error("Falha ao buscar dados do mock:", mockError);
}
}
const nextAppointment = upcoming[0];
const appointmentsThisMonth = appointments.filter(appt => {
const apptDate = new Date(appt.scheduled_at);
const now = new Date();
return apptDate.getMonth() === now.getMonth() && apptDate.getFullYear() === now.getFullYear();
});
let profileCompleteness = 0;
let profileDescription = "Dados não encontrados";
if (patientData) {
const profileFields = ['nome_completo', 'cpf', 'email', 'telefone', 'data_nascimento', 'endereco', 'cidade', 'estado', 'cep', 'convenio'];
const filledFields = profileFields.filter(field => patientData[field]).length;
profileCompleteness = Math.round((filledFields / profileFields.length) * 100);
profileDescription = profileCompleteness === 100 ? "Dados completos" : `${filledFields} de ${profileFields.length} campos preenchidos`;
}
setStatsData([
{
title: "Próxima Consulta",
value: nextAppointment ? new Date(nextAppointment.scheduled_at).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }) : "Nenhuma",
description: nextAppointment ? `${nextAppointment.doctors?.full_name || 'Médico'} - ${new Date(nextAppointment.scheduled_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })}` : "Sem consultas futuras",
icon: Calendar
},
{
title: "Consultas Este Mês",
value: appointmentsThisMonth.length.toString(),
description: `${appointmentsThisMonth.filter(a => a.status === 'completed').length} realizadas`,
icon: Clock
},
{
title: "Perfil",
value: `${profileCompleteness}%`,
description: profileDescription,
icon: User
},
]);
} catch (err) {
console.error("Erro geral ao carregar dados do dashboard:", err);
setError("Não foi possível carregar as informações. Tente novamente mais tarde.");
} finally {
setIsLoading(false);
}
};
fetchData();
}, []);
if (isLoading) {
return <div className="text-center text-muted-foreground">Carregando dashboard...</div>;
}
if (error) {
return <div className="text-center text-destructive p-4 bg-destructive/10 rounded-md">{error}</div>;
}
return (
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Dashboard</h1>
<p className="text-muted-foreground">Bem-vindo ao seu portal de consultas médicas.</p>
</div>
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
{statsData.map((stat) => (
<DashboardStatCard key={stat.title} {...stat} />
))}
</div>
<div className="grid md:grid-cols-2 gap-6">
<Card>
<CardHeader>
<CardTitle>Ações Rápidas</CardTitle>
<CardDescription>Acesse rapidamente as principais funcionalidades.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
{quickActions.map((action) => {
const Icon = action.icon;
return (
<Link key={action.href} href={action.href}>
<Button variant={action.variant} className="w-full justify-start bg-transparent">
<Icon className="mr-2 h-4 w-4" />
{action.label}
</Button>
</Link>
);
})}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Próximas Consultas</CardTitle>
<CardDescription>Suas consultas agendadas para o futuro.</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-4">
{upcomingAppointments.length > 0 ? (
upcomingAppointments.slice(0, 5).map((appointment) => (
<UpcomingAppointmentItem key={appointment.id} appointment={{
doctorName: appointment.doctors?.full_name || 'Médico a confirmar',
specialty: appointment.doctors?.specialty || 'Especialidade',
date: new Date(appointment.scheduled_at).toLocaleDateString('pt-BR', { day: '2-digit', month: 'short' }),
time: new Date(appointment.scheduled_at).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })
}} />
))
) : (
<p className="text-sm text-muted-foreground">Você não tem nenhuma consulta agendada.</p>
)}
</div>
</CardContent>
</Card>
</div>
</div>
);
}

View File

@ -1,53 +1,53 @@
// Caminho: app/(patient)/layout.tsx
// Caminho: app/(patient)/layout.tsx (Refatoração Sugerida)
"use client";
import type React from "react";
import { useState, useEffect } from "react";
import { useState, useEffect, createContext, useContext } from "react";
import { useRouter } from "next/navigation";
// Nossas importações centralizadas
import { usuariosApi } from "@/services/usuariosApi";
import { usuariosApi, User } from "@/services/usuariosApi"; // Assumindo que User é exportado
import DashboardLayout, { UserProfile } from "@/components/layout/DashboardLayout";
import { dashboardConfig } from "@/config/dashboard.config";
interface PatientLayoutProps {
children: React.ReactNode;
// --- Contexto para compartilhar dados do usuário autenticado ---
interface PatientAuthContextType {
user: User | null;
userProfile: UserProfile | null;
isLoading: boolean;
}
export default function PatientLayout({ children }: PatientLayoutProps) {
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
const PatientAuthContext = createContext<PatientAuthContextType | undefined>(undefined);
// Hook customizado para facilitar o acesso ao contexto nas páginas filhas
export const usePatientAuth = () => {
const context = useContext(PatientAuthContext);
if (!context) {
throw new Error("usePatientAuth deve ser usado dentro de um PatientLayout");
}
return context;
};
// ----------------------------------------------------------------
export default function PatientLayout({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
useEffect(() => {
const checkAuthentication = async () => {
try {
// 1. Busca o usuário logado via API
const userData = await usuariosApi.getCurrentUser();
// 2. Pega a configuração específica do "paciente"
const config = dashboardConfig.patient;
if (!config) {
throw new Error("Configuração para o perfil 'patient' não encontrada.");
}
// 3. Formata os dados para o perfil
setUserProfile(config.getUserProfile(userData));
if (!userData) throw new Error("Usuário não autenticado.");
setUser(userData);
} catch (error) {
// 4. Se falhar, redireciona para o login
console.error("Falha na autenticação para paciente:", error);
router.push("/login");
} finally {
setIsLoading(false);
}
};
checkAuthentication();
}, [router]);
// Enquanto a verificação estiver em andamento, mostra uma tela de carregamento
if (isLoading) {
return (
<div className="flex h-screen w-full items-center justify-center bg-background">
@ -56,18 +56,20 @@ export default function PatientLayout({ children }: PatientLayoutProps) {
);
}
// Se não tiver perfil (redirect em andamento), não renderiza nada para evitar erros
if (!userProfile) {
return null;
if (!user) {
return null; // Evita renderizar o layout durante o redirecionamento
}
// Pega os itens de menu da configuração
// A formatação do perfil agora é feita aqui, com os dados já carregados
const userProfile = dashboardConfig.patient.getUserProfile(user);
const menuItems = dashboardConfig.patient.menuItems;
const contextValue = { user, userProfile, isLoading };
// Renderiza o layout genérico com as props corretas
return (
<DashboardLayout menuItems={menuItems} userProfile={userProfile}>
{children}
</DashboardLayout>
<PatientAuthContext.Provider value={contextValue}>
<DashboardLayout menuItems={menuItems} userProfile={userProfile}>
{children}
</DashboardLayout>
</PatientAuthContext.Provider>
);
}

View File

@ -1,139 +1,244 @@
"use client"
// Caminho: app/(patient)/schedule/page.tsx (Completo e Corrigido)
"use client";
import { useState, useEffect, useCallback } from "react"
import { Calendar, Clock, User } from "lucide-react"
import PatientLayout from "@/components/patient-layout"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { doctorsService } from "services/medicosApi"
import type React from "react";
import { useState, useEffect } from "react";
import { useRouter } from "next/navigation";
import { format, getDay } from "date-fns";
import { ptBR } from "date-fns/locale";
// A importação do PatientLayout foi REMOVIDA
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Calendar as CalendarIcon, Clock, User as UserIcon } from "lucide-react";
import { Calendar } from "@/components/ui/calendar";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
interface Doctor {
id: string
full_name: string
specialty: string
phone_mobile: string
import { usuariosApi, User } from "@/services/usuariosApi";
import { medicosApi, Doctor } from "@/services/medicosApi";
import { agendamentosApi } from "@/services/agendamentosApi";
import { disponibilidadeApi, DoctorAvailability, DoctorException } from "@/services/disponibilidadeApi";
interface AvailabilityRules {
weekly: DoctorAvailability[];
exceptions: DoctorException[];
}
const APPOINTMENTS_STORAGE_KEY = "clinic-appointments"
export default function ScheduleAppointment() {
const [selectedDoctor, setSelectedDoctor] = useState("")
const [selectedDate, setSelectedDate] = useState("")
const [selectedTime, setSelectedTime] = useState("")
const [notes, setNotes] = useState("")
const router = useRouter();
const [user, setUser] = useState<User | null>(null);
const [doctors, setDoctors] = useState<Doctor[]>([]);
const [availableSlots, setAvailableSlots] = useState<string[]>([]);
const [availabilityRules, setAvailabilityRules] = useState<AvailabilityRules | null>(null);
const [isAvailabilityLoading, setIsAvailabilityLoading] = useState(false);
// novos campos
const [tipoConsulta, setTipoConsulta] = useState("presencial")
const [duracao, setDuracao] = useState("30")
const [convenio, setConvenio] = useState("")
const [queixa, setQueixa] = useState("")
const [obsPaciente, setObsPaciente] = useState("")
const [obsInternas, setObsInternas] = useState("")
const [formData, setFormData] = useState<{
doctorId: string;
date: Date | undefined;
time: string;
appointmentType: string;
duration: string;
reason: string;
}>({
doctorId: "",
date: undefined,
time: "",
appointmentType: "presencial",
duration: "30",
reason: "",
});
const [doctors, setDoctors] = useState<Doctor[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchDoctors = useCallback(async () => {
setLoading(true)
setError(null)
try {
const data: Doctor[] = await doctorsService.list()
setDoctors(data || [])
} catch (e: any) {
console.error("Erro ao carregar lista de médicos:", e)
setError("Não foi possível carregar a lista de médicos. Verifique a conexão com a API.")
setDoctors([])
} finally {
setLoading(false)
}
}, [])
const [isLoading, setIsLoading] = useState(true);
const [isSlotsLoading, setIsSlotsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchDoctors()
}, [fetchDoctors])
const loadInitialData = async () => {
try {
const currentUser = await usuariosApi.getCurrentUser();
setUser(currentUser);
const availableTimes = [
"08:00",
"08:30",
"09:00",
"09:30",
"10:00",
"10:30",
"14:00",
"14:30",
"15:00",
"15:30",
"16:00",
"16:30",
]
let activeDoctors = await medicosApi.list({ active: true });
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
if (activeDoctors.length === 0) {
console.warn("Nenhum médico ativo encontrado. Buscando do mock...");
toast.info("Usando dados de exemplo para a lista de médicos.");
activeDoctors = await medicosApi.getMockDoctors();
}
setDoctors(activeDoctors);
} catch (e) {
console.error("Erro ao carregar dados iniciais:", e);
setError("Não foi possível carregar os dados necessários para o agendamento.");
} finally {
setIsLoading(false);
}
};
loadInitialData();
}, []);
const doctorDetails = doctors.find((d) => d.id === selectedDoctor)
const patientDetails = {
id: "P001",
full_name: "Paciente Exemplo Único",
location: "Clínica Geral",
phone: "(11) 98765-4321",
useEffect(() => {
const fetchDoctorAvailability = async () => {
if (!formData.doctorId) {
setAvailabilityRules(null);
return;
}
if (formData.doctorId.startsWith("mock-")) {
setAvailabilityRules({ weekly: [], exceptions: [] });
return;
}
setIsAvailabilityLoading(true);
try {
const [weekly, exceptions] = await Promise.all([
disponibilidadeApi.list({ doctor_id: formData.doctorId, active: true }),
disponibilidadeApi.listExceptions({ doctor_id: formData.doctorId }),
]);
setAvailabilityRules({ weekly, exceptions });
} catch (err) {
console.error("Erro ao buscar disponibilidade do médico:", err);
toast.error("Não foi possível carregar a agenda do médico.");
setAvailabilityRules(null);
} finally {
setIsAvailabilityLoading(false);
}
};
fetchDoctorAvailability();
}, [formData.doctorId]);
const fetchAvailableSlots = (doctorId: string, date: Date | undefined) => {
if (!doctorId || !date) return;
setIsSlotsLoading(true);
setAvailableSlots([]);
if (doctorId.startsWith("mock-")) {
setTimeout(() => {
const mockSlots = ["09:00", "10:00", "11:00", "14:00", "15:00"];
setAvailableSlots(mockSlots);
setIsSlotsLoading(false);
}, 500);
return;
}
const formattedDate = format(date, "yyyy-MM-dd");
agendamentosApi.getAvailableSlots(doctorId, formattedDate)
.then(response => {
const slots = response.slots.filter(s => s.available).map(s => s.time);
setAvailableSlots(slots);
})
.catch(() => toast.error("Não foi possível buscar horários para esta data."))
.finally(() => setIsSlotsLoading(false));
};
const handleSelectChange = (name: keyof typeof formData) => (value: string | Date | undefined) => {
const newFormData = { ...formData, [name]: value } as any;
if (name === 'doctorId') {
newFormData.date = undefined;
newFormData.time = "";
}
if (name === 'date') {
newFormData.time = "";
fetchAvailableSlots(newFormData.doctorId, newFormData.date);
}
setFormData(newFormData);
};
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { id, value } = e.target;
setFormData(prev => ({ ...prev, [id]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!user?.id || !formData.date) {
toast.error("Erro de autenticação ou data inválida.");
return;
}
if (formData.doctorId.startsWith("mock-")) {
toast.success("Simulação de agendamento com médico de exemplo concluída!");
router.push("/patient/appointments");
return;
}
if (!patientDetails || !doctorDetails) {
alert("Erro: Selecione o médico ou dados do paciente indisponíveis.")
return
setIsSubmitting(true);
try {
const newScheduledAt = new Date(`${format(formData.date, "yyyy-MM-dd")}T${formData.time}:00Z`).toISOString();
await agendamentosApi.create({
doctor_id: formData.doctorId,
patient_id: user.id,
scheduled_at: newScheduledAt,
duration_minutes: parseInt(formData.duration, 10),
appointment_type: formData.appointmentType as 'presencial' | 'telemedicina',
status: "requested",
created_by: user.id,
notes: formData.reason,
});
toast.success("Consulta agendada com sucesso!");
router.push("/patient/appointments");
} catch (error) {
console.error("Erro ao agendar consulta:", error);
toast.error("Falha ao agendar a consulta. Tente novamente.");
} finally {
setIsSubmitting(false);
}
};
const isDateDisabled = (date: Date): boolean => {
if (date < new Date(new Date().setDate(new Date().getDate() - 1))) {
return true;
}
const newAppointment = {
id: new Date().getTime(),
patientName: patientDetails.full_name,
doctor: doctorDetails.full_name,
specialty: doctorDetails.specialty,
date: selectedDate,
time: selectedTime,
tipoConsulta,
duracao,
convenio,
queixa,
obsPaciente,
obsInternas,
notes,
status: "agendada",
phone: patientDetails.phone,
if (!availabilityRules) {
return false;
}
const storedAppointmentsRaw = localStorage.getItem(APPOINTMENTS_STORAGE_KEY)
const currentAppointments = storedAppointmentsRaw ? JSON.parse(storedAppointmentsRaw) : []
const updatedAppointments = [...currentAppointments, newAppointment]
localStorage.setItem(APPOINTMENTS_STORAGE_KEY, JSON.stringify(updatedAppointments))
const dateString = format(date, "yyyy-MM-dd");
const dayOfWeek = getDay(date);
alert(`Consulta com ${doctorDetails.full_name} agendada com sucesso!`)
const fullDayBlock = availabilityRules.exceptions.find(
ex => ex.date === dateString && ex.kind === 'bloqueio' && !ex.start_time
);
if (fullDayBlock) {
return true;
}
// resetar campos
setSelectedDoctor("")
setSelectedDate("")
setSelectedTime("")
setNotes("")
setTipoConsulta("presencial")
setDuracao("30")
setConvenio("")
setQueixa("")
setObsPaciente("")
setObsInternas("")
}
const worksOnThisDay = availabilityRules.weekly.some(
avail => avail.weekday === dayOfWeek
);
return !worksOnThisDay;
};
const selectedDoctorDetails = doctors.find((d) => d.id === formData.doctorId);
const isFormInvalid = !formData.doctorId || !formData.date || !formData.time || isSubmitting;
return (
<PatientLayout>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold text-gray-900">Agendar Consulta</h1>
<p className="text-gray-600">Escolha o médico, data e horário para sua consulta</p>
</div>
<div className="space-y-6">
<div>
<h1 className="text-3xl font-bold">Agendar Consulta</h1>
<p className="text-muted-foreground">Escolha o médico, data e horário para sua consulta</p>
</div>
{isLoading ? (
<p className="text-muted-foreground">Carregando...</p>
) : error ? (
<p className="text-destructive">{error}</p>
) : (
<div className="grid lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card>
@ -143,203 +248,112 @@ export default function ScheduleAppointment() {
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Médico */}
<div className="space-y-2">
<Label htmlFor="doctor">Médico</Label>
<Select value={selectedDoctor} onValueChange={setSelectedDoctor}>
<SelectTrigger>
<SelectValue placeholder="Selecione um médico" />
</SelectTrigger>
<Label htmlFor="doctorId">Médico</Label>
<Select value={formData.doctorId} onValueChange={handleSelectChange('doctorId')}>
<SelectTrigger><SelectValue placeholder="Selecione um médico" /></SelectTrigger>
<SelectContent>
{loading ? (
<SelectItem value="loading" disabled>
Carregando médicos...
</SelectItem>
) : error ? (
<SelectItem value="error" disabled>
Erro ao carregar
</SelectItem>
) : (
{doctors.length > 0 ? (
doctors.map((doctor) => (
<SelectItem key={doctor.id} value={doctor.id}>
{doctor.full_name} - {doctor.specialty}
</SelectItem>
))
) : (
<div className="p-2 text-center text-sm text-muted-foreground">Nenhum médico disponível</div>
)}
</SelectContent>
</Select>
</div>
{/* Data e horário */}
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="date">Data</Label>
<Input
id="date"
type="date"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
min={new Date().toISOString().split("T")[0]}
/>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn("w-full justify-start text-left font-normal", !formData.date && "text-muted-foreground")}
disabled={!formData.doctorId || isAvailabilityLoading}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{isAvailabilityLoading ? "Carregando agenda..." : formData.date ? format(formData.date, "PPP", { locale: ptBR }) : <span>Escolha uma data</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={formData.date}
onSelect={handleSelectChange('date')}
initialFocus
disabled={isDateDisabled}
/>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label htmlFor="time">Horário</Label>
<Select value={selectedTime} onValueChange={setSelectedTime}>
<Select value={formData.time} onValueChange={handleSelectChange('time')} disabled={!formData.date || isSlotsLoading}>
<SelectTrigger>
<SelectValue placeholder="Selecione um horário" />
<SelectValue placeholder={isSlotsLoading ? "Carregando..." : "Selecione um horário"} />
</SelectTrigger>
<SelectContent>
{availableTimes.map((time) => (
<SelectItem key={time} value={time}>
{time}
</SelectItem>
))}
{isSlotsLoading ? (
<div className="p-2 text-center text-sm text-muted-foreground">Carregando horários...</div>
) : availableSlots.length > 0 ? (
availableSlots.map((time) => <SelectItem key={time} value={time}>{time}</SelectItem>)
) : (
<div className="p-2 text-center text-sm text-muted-foreground">
{formData.date ? "Nenhum horário disponível" : "Selecione uma data"}
</div>
)}
</SelectContent>
</Select>
</div>
</div>
{/* Tipo e Duração */}
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="tipoConsulta">Tipo de Consulta</Label>
<Select value={tipoConsulta} onValueChange={setTipoConsulta}>
<SelectTrigger id="tipoConsulta">
<SelectValue placeholder="Selecione o tipo" />
</SelectTrigger>
<Label htmlFor="appointmentType">Tipo de Consulta</Label>
<Select value={formData.appointmentType} onValueChange={handleSelectChange('appointmentType')}>
<SelectTrigger id="appointmentType"><SelectValue placeholder="Selecione o tipo" /></SelectTrigger>
<SelectContent>
<SelectItem value="presencial">Presencial</SelectItem>
<SelectItem value="online">Telemedicina</SelectItem>
<SelectItem value="telemedicina">Telemedicina</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="duracao">Duração (minutos)</Label>
<Input
id="duracao"
type="number"
min={10}
max={120}
value={duracao}
onChange={(e) => setDuracao(e.target.value)}
/>
<Label htmlFor="duration">Duração (minutos)</Label>
<Input id="duration" type="number" min={10} max={120} value={formData.duration} onChange={handleInputChange} />
</div>
</div>
{/* Convênio */}
<div className="space-y-2">
<Label htmlFor="convenio">Convênio (opcional)</Label>
<Input
id="convenio"
placeholder="Nome do convênio do paciente"
value={convenio}
onChange={(e) => setConvenio(e.target.value)}
/>
<Label htmlFor="reason">Queixa Principal / Observações (opcional)</Label>
<Textarea id="reason" placeholder="Descreva brevemente o motivo da consulta ou observações importantes..." value={formData.reason} onChange={handleInputChange} rows={3} />
</div>
{/* Queixa Principal */}
<div className="space-y-2">
<Label htmlFor="queixa">Queixa Principal (opcional)</Label>
<Textarea
id="queixa"
placeholder="Descreva brevemente o motivo da consulta..."
value={queixa}
onChange={(e) => setQueixa(e.target.value)}
/>
</div>
{/* Observações do Paciente */}
<div className="space-y-2">
<Label htmlFor="obsPaciente">Observações do Paciente (opcional)</Label>
<Textarea
id="obsPaciente"
placeholder="Anotações relevantes informadas pelo paciente..."
value={obsPaciente}
onChange={(e) => setObsPaciente(e.target.value)}
/>
</div>
{/* Observações Internas */}
<div className="space-y-2">
<Label htmlFor="obsInternas">Observações Internas (opcional)</Label>
<Textarea
id="obsInternas"
placeholder="Anotações para a equipe da clínica..."
value={obsInternas}
onChange={(e) => setObsInternas(e.target.value)}
/>
</div>
{/* Observações gerais */}
<div className="space-y-2">
<Label htmlFor="notes">Observações gerais (opcional)</Label>
<Textarea
id="notes"
placeholder="Descreva brevemente o motivo da consulta ou observações importantes"
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
/>
</div>
{/* Botão */}
<Button
type="submit"
className="w-full"
disabled={!selectedDoctor || !selectedDate || !selectedTime}
>
Agendar Consulta
<Button type="submit" className="w-full" disabled={isFormInvalid}>
{isSubmitting ? "Agendando..." : "Agendar Consulta"}
</Button>
</form>
</CardContent>
</Card>
</div>
{/* Resumo */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center">
<Calendar className="mr-2 h-5 w-5" />
Resumo
</CardTitle>
</CardHeader>
<CardHeader><CardTitle className="flex items-center"><CalendarIcon className="mr-2 h-5 w-5" /> Resumo</CardTitle></CardHeader>
<CardContent className="space-y-4">
{selectedDoctor && (
<div className="flex items-center space-x-2">
<User className="h-4 w-4 text-gray-500" />
<span className="text-sm">
{doctors.find((d) => d.id === selectedDoctor)?.full_name}
</span>
</div>
)}
{selectedDate && (
<div className="flex items-center space-x-2">
<Calendar className="h-4 w-4 text-gray-500" />
<span className="text-sm">
{new Date(selectedDate).toLocaleDateString("pt-BR")}
</span>
</div>
)}
{selectedTime && (
<div className="flex items-center space-x-2">
<Clock className="h-4 w-4 text-gray-500" />
<span className="text-sm">{selectedTime}</span>
</div>
)}
{selectedDoctorDetails && <div className="flex items-center space-x-2"><UserIcon className="h-4 w-4 text-muted-foreground" /><span className="text-sm">{selectedDoctorDetails.full_name}</span></div>}
{formData.date && <div className="flex items-center space-x-2"><CalendarIcon className="h-4 w-4 text-muted-foreground" /><span className="text-sm">{format(formData.date, "PPP", { locale: ptBR })}</span></div>}
{formData.time && <div className="flex items-center space-x-2"><Clock className="h-4 w-4 text-muted-foreground" /><span className="text-sm">{formData.time}</span></div>}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Informações Importantes</CardTitle>
</CardHeader>
<CardContent className="text-sm text-gray-600 space-y-2">
<CardHeader><CardTitle>Informações Importantes</CardTitle></CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-2">
<p> Chegue com 15 minutos de antecedência</p>
<p> Traga documento com foto</p>
<p> Traga carteirinha do convênio</p>
@ -348,7 +362,7 @@ export default function ScheduleAppointment() {
</Card>
</div>
</div>
</div>
</PatientLayout>
)
}
)}
</div>
);
}

View File

@ -45,7 +45,7 @@ export function LoginForm({ children }: LoginFormProps) {
const handleRoleSelection = (selectedDashboardRole: string) => {
const user = authenticatedUser;
if (!user) {
toast({ title: "Erro de Sessão", description: "Não foi possível encontrar os dados do usuário. Tente novamente.", variant: "destructive" });
toast({ title: "Erro de Sessão", description: "Não foi possível encontrar os dados do usuário. Tente novamente."});
setUserRoles([]); // Volta para a tela de login
return;
}
@ -68,7 +68,7 @@ export function LoginForm({ children }: LoginFormProps) {
toast({ title: `Entrando como ${selectedDashboardRole}...` });
router.push(redirectPath);
} else {
toast({ title: "Erro", description: "Perfil selecionado inválido.", variant: "destructive" });
toast({ title: "Erro", description: "Perfil selecionado inválido."});
}
};
@ -148,8 +148,7 @@ export function LoginForm({ children }: LoginFormProps) {
toast({
title: "Erro no Login",
description: error instanceof Error ? error.message : "Ocorreu um erro inesperado.",
variant: "destructive",
description: error instanceof Error ? error.message : "Ocorreu um erro inesperado."
});
} finally {
setIsLoading(false);

View File

@ -1,3 +1,5 @@
// Caminho: components/ui/button.tsx (Completo e Corrigido)
import * as React from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
@ -35,25 +37,25 @@ const buttonVariants = cva(
},
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
export { Button, buttonVariants }
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -31,7 +31,7 @@ const toastVariants = cva(
variant: {
default: 'border bg-background text-foreground',
destructive:
'destructive group border-destructive bg-destructive text-destructive-foreground',
'destructive group border-destructive bg-destructive text-foreground',
},
},
defaultVariants: {

View File

@ -25,11 +25,11 @@ interface RoleConfig {
export const dashboardConfig: Record<string, RoleConfig> = {
doctor: {
menuItems: [
{ href: "/doctor/dashboard", icon: Home, label: "Dashboard" },
{ href: "/doctor/medicos/consultas", icon: Calendar, label: "Consultas" },
{ href: "#", icon: Clock, label: "Editor de Laudo" },
{ href: "/doctor/medicos", icon: User, label: "Pacientes" },
{ href: "/doctor/disponibilidade", icon: Calendar, label: "Disponibilidade" },
{ href: "/medicos/doctor/dashboard", icon: Home, label: "Dashboard" },
{ href: "/medicos/doctor/medicos/consultas", icon: Calendar, label: "Consultas" },
{ href: "/medicos/doctor/[id]/laudos", icon: Clock, label: "Editor de Laudo" },
{ href: "/medicos/doctor/medicos", icon: User, label: "Pacientes" },
{ href: "/medicos/doctor/disponibilidade", icon: Calendar, label: "Disponibilidade" },
],
getUserProfile: (userInfo) => getProfile(userInfo, { name: "Doutor(a)", secondaryText: "Especialidade" }),
},
@ -45,20 +45,21 @@ export const dashboardConfig: Record<string, RoleConfig> = {
},
secretary: {
menuItems: [
{ href: "/secretary/dashboard", icon: Home, label: "Dashboard" },
{ href: "/secretary/appointments", icon: Calendar, label: "Consultas" },
{ href: "/secretary/schedule", icon: Clock, label: "Agendar Consulta" },
{ href: "/secretary/pacientes", icon: User, label: "Pacientes" },
],
{ href: "/manager/dashboard", icon: Home, label: "Dashboard" },
{ href: "/manager/relatorios", icon: Calendar, label: "Relatórios gerenciais" },
{ href: "/manager/usuario", icon: User, label: "Gestão de Usuários" },
{ href: "/manager/home", icon: User, label: "Gestão de Médicos" },
{ href: "/manager/configuracoes", icon: Calendar, label: "Configurações" },
],
getUserProfile: (userInfo) => getProfile(userInfo, { name: "Secretária", secondaryText: "Atendimento" }),
},
manager: {
menuItems: [
{ href: "#dashboard", icon: Home, label: "Dashboard" },
{ href: "#reports", icon: Calendar, label: "Relatórios gerenciais" },
{ href: "#users", icon: User, label: "Gestão de Usuários" },
{ href: "#doctors", icon: User, label: "Gestão de Médicos" },
{ href: "#settings", icon: Calendar, label: "Configurações" },
{ href: "/manager/dashboard", icon: Home, label: "Dashboard" },
{ href: "#", icon: Calendar, label: "Relatórios gerenciais" },
{ href: "/manager/usuario", icon: User, label: "Gestão de Usuários" },
{ href: "/manager/home", icon: User, label: "Gestão de Médicos" },
{ href: "#", icon: Calendar, label: "Configurações" },
],
getUserProfile: (userInfo) => getProfile(userInfo, { name: "Gestor(a)", secondaryText: "Gestão" }),
},

18
package-lock.json generated
View File

@ -71,13 +71,13 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.1.9",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.18.10",
"@types/node": "^22.18.12",
"@types/react": "^18.3.26",
"@types/react-dom": "^18",
"@types/react-dom": "^18.3.7",
"postcss": "^8.5",
"tailwindcss": "^4.1.9",
"tw-animate-css": "1.3.3",
"typescript": "^5"
"typescript": "^5.9.3"
}
},
"node_modules/@alloc/quick-lru": {
@ -2497,9 +2497,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.18.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.10.tgz",
"integrity": "sha512-anNG/V/Efn/YZY4pRzbACnKxNKoBng2VTFydVu8RRs5hQjikP8CQfaeAV59VFSCzKNp90mXiVXW2QzV56rwMrg==",
"version": "22.18.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.12.tgz",
"integrity": "sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4508,9 +4508,9 @@
}
},
"node_modules/typescript": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@ -72,12 +72,12 @@
"devDependencies": {
"@tailwindcss/postcss": "^4.1.9",
"@types/js-cookie": "^3.0.6",
"@types/node": "^22.18.10",
"@types/node": "^22.18.12",
"@types/react": "^18.3.26",
"@types/react-dom": "^18",
"@types/react-dom": "^18.3.7",
"postcss": "^8.5",
"tailwindcss": "^4.1.9",
"tw-animate-css": "1.3.3",
"typescript": "^5"
"typescript": "^5.9.3"
}
}

View File

@ -1,54 +1,95 @@
// Caminho: services/agendamentosApi.ts
// Caminho: services/agendamentosApi.ts (Completo, com adição de update e delete)
import api from './api';
export interface Appointment {
id: any;
patient_id: string;
id: string;
doctor_id: string;
patient_id: string;
scheduled_at: string;
[key: string]: any;
duration_minutes?: number;
status: 'requested' | 'confirmed' | 'completed' | 'cancelled';
created_by?: string;
cancel_reason?: string;
reschedule_reason?: string;
appointment_type?: 'presencial' | 'telemedicina';
notes?: string;
doctors?: {
full_name: string;
specialty: string;
};
}
interface AvailableSlotsData {
doctor_id: string;
start_date: string;
end_date: string;
appointment_type?: 'presencial' | 'telemedicina';
export type CreateAppointmentData = Omit<Appointment, 'id' | 'doctors' | 'cancel_reason' | 'reschedule_reason'>;
export type UpdateAppointmentData = Partial<Omit<Appointment, 'id' | 'doctors' | 'created_by' | 'patient_id' | 'doctor_id'>>;
export interface AvailableSlot {
time: string;
available: boolean;
}
export const agendamentosApi = {
list: async (): Promise<Appointment[]> => {
const response = await api.get<Appointment[]>('/rest/v1/appointments?select=*');
return response.data;
},
getById: async (id: string): Promise<Appointment> => {
const response = await api.get<Appointment>(`/rest/v1/appointments?id=eq.${id}&select=*`, {
headers: { Accept: 'application/vnd.pgrst.object+json' },
listByPatient: async (patientId: string): Promise<Appointment[]> => {
const response = await api.get<Appointment[]>('/rest/v1/appointments', {
params: {
patient_id: `eq.${patientId}`,
select: '*,doctors(full_name,specialty)',
order: 'scheduled_at.asc',
},
});
return response.data;
},
create: async (data: Omit<Appointment, 'id'>): Promise<Appointment> => {
create: async (data: CreateAppointmentData): Promise<Appointment> => {
const response = await api.post<Appointment[]>('/rest/v1/appointments', data, {
headers: { 'Prefer': 'return=representation' }
headers: { 'Prefer': 'return=representation' },
});
return response.data[0];
},
update: async (id: string, data: Partial<Appointment>): Promise<Appointment> => {
/**
* Atualiza um agendamento existente (PATCH).
* @param id - O UUID do agendamento.
* @param data - Os campos a serem atualizados.
*/
update: async (id: string, data: UpdateAppointmentData): Promise<Appointment> => {
const response = await api.patch<Appointment[]>(`/rest/v1/appointments?id=eq.${id}`, data, {
headers: { 'Prefer': 'return=representation' }
headers: {
'Prefer': 'return=representation',
},
});
return response.data[0];
},
/**
* Exclui um agendamento.
* @param id - O UUID do agendamento a ser excluído.
*/
delete: async (id: string): Promise<void> => {
await api.delete(`/rest/v1/appointments?id=eq.${id}`);
},
searchAvailableSlots: async (data: AvailableSlotsData): Promise<any> => {
const response = await api.post('/functions/v1/get-available-slots', data);
getAvailableSlots: async (doctorId: string, date: string): Promise<{ slots: AvailableSlot[] }> => {
const response = await api.post<{ slots: AvailableSlot[] }>('/functions/v1/get-available-slots', {
doctor_id: doctorId,
date: date,
});
return response.data;
},
getMockAppointments: async (): Promise<Appointment[]> => {
const response = await api.get<any[]>('https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctors');
return response.data.map((doctor: any, index: number) => ({
id: `mock-${index + 1}`,
doctor_id: doctor.id || `doc-mock-${index + 1}`,
patient_id: 'patient-mock-1',
scheduled_at: new Date(Date.now() + 86400000 * (index + 2)).toISOString(),
status: index % 2 === 0 ? 'confirmed' : 'requested',
appointment_type: index % 2 === 0 ? 'presencial' : 'telemedicina',
doctors: {
full_name: doctor.full_name || `Dr. Exemplo ${index + 1}`,
specialty: doctor.specialty || 'Especialidade',
},
}));
},
};

221
services/api/apiService.ts Normal file
View File

@ -0,0 +1,221 @@
// apiService.ts (V4)
import {
AuthResponse, UserProfile, Patient, Doctor, Appointment, NewAppointmentPayload,
AvailableSlot, Report, ReportInput, DoctorAvailability, DoctorException,
NetworkError, LoginResponse, SendMagicLinkResponse, LogoutResponse,
GetCurrentUserResponse, RequestPasswordResetResponse, CreateUserWithPasswordResponse,
HardDeleteUserResponse, RegisterPatientResponse, GetAvailableSlotsResponse,
CreateAppointmentResponse, CancelAppointmentResponse, CreateReportResponse,
UpdateReportResponse, ListResponse
} from './types';
// Ação Futura: Mover estas chaves para variáveis de ambiente.
const BASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://yuanqfswhberkoevtmfr.supabase.co';
const API_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ';
/**
* Cliente de API base que retorna uma união discriminada para todos os cenários.
*/
async function apiClient<T>(endpoint: string, options: RequestInit = {}, isPublic: boolean = false): Promise<{ status: number; data: T } | NetworkError> {
const headers = new Headers(options.headers || {});
headers.set('apikey', API_KEY);
headers.set('Content-Type', 'application/json');
if (!isPublic) {
try {
const authSession = localStorage.getItem('supabase.auth.token');
if (authSession) {
const session = JSON.parse(authSession);
// A estrutura do token pode variar, ajuste se necessário
const token = session?.currentSession?.access_token || session?.access_token;
if (token) {
headers.set('Authorization', `Bearer ${token}`);
}
}
} catch (error) {
console.error("Falha ao ler o token de autenticação do localStorage.", error);
}
}
const config: RequestInit = { ...options, headers };
try {
const response = await fetch(`${BASE_URL}${endpoint}`, config);
if (response.status === 204) {
return { status: response.status, data: undefined as T };
}
const data = await response.json().catch(() => ({}));
return {
status: response.status,
data: data as T,
};
} catch (error) {
if (error instanceof Error) {
return { status: 'network_error', error };
}
return { status: 'network_error', error: new Error('Erro de rede desconhecido') };
}
}
// V4 CHANGE: Adicionamos uma asserção de tipo `as Promise<...>` em cada função.
// Isso informa ao TypeScript que confiamos que o retorno do apiClient corresponderá
// à união discriminada específica que definimos para cada endpoint.
// --- SERVIÇOS DE AUTENTICAÇÃO ---
export const authService = {
login: (credentials: { email: string; password: string }): Promise<LoginResponse> => {
return apiClient('/auth/v1/token?grant_type=password', {
method: 'POST',
body: JSON.stringify(credentials),
}, true) as Promise<LoginResponse>;
},
sendMagicLink: (email: string): Promise<SendMagicLinkResponse> => {
return apiClient('/auth/v1/otp', {
method: 'POST',
body: JSON.stringify({ email }),
}, true) as Promise<SendMagicLinkResponse>;
},
logout: (): Promise<LogoutResponse> => {
return apiClient('/auth/v1/logout', { method: 'POST' }) as Promise<LogoutResponse>;
},
getCurrentUser: (): Promise<GetCurrentUserResponse> => {
return apiClient('/functions/v1/user-info', { method: 'POST' }) as Promise<GetCurrentUserResponse>;
},
};
// --- SERVIÇOS DE USUÁRIOS ---
export const userService = {
requestPasswordReset: (email: string, redirectUrl?: string): Promise<RequestPasswordResetResponse> => {
return apiClient('/request-password-reset', {
method: 'POST',
body: JSON.stringify({ email, redirect_url: redirectUrl }),
}, true) as Promise<RequestPasswordResetResponse>;
},
createUserWithPassword: (payload: object): Promise<CreateUserWithPasswordResponse> => {
return apiClient('/create-user-with-password', {
method: 'POST',
body: JSON.stringify(payload),
}) as Promise<CreateUserWithPasswordResponse>;
},
hardDeleteUser_DANGEROUS: (userId: string): Promise<HardDeleteUserResponse> => {
return apiClient('/delete-user', {
method: 'POST',
body: JSON.stringify({ userId }),
}) as Promise<HardDeleteUserResponse>;
},
deactivateUser: (userId: string): Promise<any> => {
return apiClient(`/rest/v1/profiles?id=eq.${userId}`, {
method: 'PATCH',
headers: { Prefer: 'return=representation' },
body: JSON.stringify({ disabled: true }),
});
}
};
// --- SERVIÇOS DE PACIENTES ---
export const patientService = {
registerPatient: (payload: { email: string; full_name: string; phone_mobile: string; cpf: string; birth_date?: string }): Promise<RegisterPatientResponse> => {
return apiClient('/functions/v1/register-patient', {
method: 'POST',
body: JSON.stringify(payload),
}, true) as Promise<RegisterPatientResponse>;
},
list: (filters: { fullName?: string; cpf?: string; limit?: number; offset?: number } = {}): Promise<ListResponse<Patient>> => {
const query = new URLSearchParams();
if (filters.fullName) query.set('full_name', `ilike.*${filters.fullName}*`);
if (filters.cpf) query.set('cpf', `eq.${filters.cpf}`);
if (filters.limit) query.set('limit', String(filters.limit));
if (filters.offset) query.set('offset', String(filters.offset));
return apiClient(`/rest/v1/patients?${query.toString()}`) as Promise<ListResponse<Patient>>;
},
create: (payload: Omit<Patient, 'id' | 'created_at' | 'updated_at'>): Promise<any> => {
return apiClient('/rest/v1/patients', {
method: 'POST',
headers: { Prefer: 'return=representation' },
body: JSON.stringify(payload),
});
},
};
// --- SERVIÇOS DE MÉDICOS ---
export const doctorService = {
list: (filters: { specialty?: string; active?: boolean } = {}): Promise<ListResponse<Doctor>> => {
const query = new URLSearchParams({ select: '*' });
if (filters.specialty) query.set('specialty', `eq.${filters.specialty}`);
if (filters.active !== undefined) query.set('active', `eq.${filters.active}`);
return apiClient(`/rest/v1/doctors?${query.toString()}`) as Promise<ListResponse<Doctor>>;
},
};
// --- SERVIÇOS DE AGENDAMENTO E DISPONIBILIDADE ---
export const scheduleService = {
getAvailableSlots: (doctorId: string, date: string): Promise<GetAvailableSlotsResponse> => {
return apiClient('/functions/v1/get-available-slots', {
method: 'POST',
body: JSON.stringify({ doctor_id: doctorId, date }),
}) as Promise<GetAvailableSlotsResponse>;
},
createAppointment: (payload: NewAppointmentPayload): Promise<CreateAppointmentResponse> => {
return apiClient('/rest/v1/appointments', {
method: 'POST',
headers: { Prefer: 'return=representation' },
body: JSON.stringify(payload),
}) as Promise<CreateAppointmentResponse>;
},
listAppointments: (filters: { doctorId?: string; patientId?: string; status?: string }): Promise<ListResponse<Appointment>> => {
const query = new URLSearchParams();
if (filters.doctorId) query.set('doctor_id', `eq.${filters.doctorId}`);
if (filters.patientId) query.set('patient_id', `eq.${filters.patientId}`);
if (filters.status) query.set('status', `eq.${filters.status}`);
return apiClient(`/rest/v1/appointments?${query.toString()}`) as Promise<ListResponse<Appointment>>;
},
cancelAppointment: (appointmentId: string, reason: string): Promise<CancelAppointmentResponse> => {
return apiClient(`/rest/v1/appointments?id=eq.${appointmentId}`, {
method: 'PATCH',
headers: { Prefer: 'return=representation' },
body: JSON.stringify({
status: 'cancelled',
cancellation_reason: reason,
cancelled_at: new Date().toISOString(),
}),
}) as Promise<CancelAppointmentResponse>;
},
listAvailability: (filters: { doctorId?: string } = {}): Promise<ListResponse<DoctorAvailability>> => {
const query = new URLSearchParams();
if (filters.doctorId) query.set('doctor_id', `eq.${filters.doctorId}`);
return apiClient(`/rest/v1/doctor_availability?${query.toString()}`) as Promise<ListResponse<DoctorAvailability>>;
},
listExceptions: (filters: { doctorId?: string; date?: string } = {}): Promise<ListResponse<DoctorException>> => {
const query = new URLSearchParams();
if (filters.doctorId) query.set('doctor_id', `eq.${filters.doctorId}`);
if (filters.date) query.set('date', `eq.${filters.date}`);
return apiClient(`/rest/v1/doctor_exceptions?${query.toString()}`) as Promise<ListResponse<DoctorException>>;
},
};
// --- SERVIÇOS DE LAUDOS (REPORTS) ---
export const reportService = {
list: (filters: { patientId?: string; createdBy?: string }): Promise<ListResponse<Report>> => {
const query = new URLSearchParams({ order: 'created_at.desc' });
if (filters.patientId) query.set('patient_id', `eq.${filters.patientId}`);
if (filters.createdBy) query.set('created_by', `eq.${filters.createdBy}`);
return apiClient(`/rest/v1/reports?${query.toString()}`) as Promise<ListResponse<Report>>;
},
create: (payload: ReportInput): Promise<CreateReportResponse> => {
return apiClient('/rest/v1/reports', {
method: 'POST',
headers: { Prefer: 'return=representation' },
body: JSON.stringify(payload),
}) as Promise<CreateReportResponse>;
},
update: (reportId: string, payload: Partial<ReportInput>): Promise<UpdateReportResponse> => {
return apiClient(`/rest/v1/reports?id=eq.${reportId}`, {
method: 'PATCH',
headers: { Prefer: 'return=representation' },
body: JSON.stringify(payload),
}) as Promise<UpdateReportResponse>;
},
};

View File

@ -0,0 +1,61 @@
// services/api/apiTestData.ts
/**
* Este arquivo centraliza todos os payloads de teste para o ApiVerificationPage.
* Cada objeto exportado contém cenários de sucesso e de erro para uma função específica.
*/
// --- Autenticação ---
export const loginTestData = {
success: { email: 'riseup@popcode.com.br', password: 'riseup' },
error: { email: 'erro@popcode.com.br', password: 'senhaerrada' },
};
export const magicLinkTestData = {
success: { email: 'gabriel.doria@popcode.com.br' }, // Use um email real que você possa verificar
error: { email: 'emailinvalido' },
};
// --- Usuários ---
export const resetPassTestData = {
success: { email: 'gabriel.doria@popcode.com.br', redirectUrl: '' },
error: { email: 'emailinvalido', redirectUrl: '' },
};
export const deleteUserTestData = {
success: { userId: 'uuid-de-um-usuario-para-deletar' }, // Substitua por um UUID real para testar
error: { userId: 'uuid-invalido' },
};
// --- Pacientes ---
export const registerPatientTestData = {
success: {
email: `paciente_${Date.now()}@teste.com`,
full_name: 'Paciente de Teste Válido',
phone_mobile: '11987654321',
cpf: '12345678901', // A API valida o formato, não a existência real
},
errorValidation: {
email: 'emailinvalido',
full_name: 'AB', // Nome curto
phone_mobile: '123', // Telefone curto
cpf: '111', // CPF curto
},
errorConflict: {
email: 'paciente_existente@teste.com', // Use um email que já exista no seu banco
full_name: 'Paciente Conflitante',
phone_mobile: '11987654321',
cpf: '11111111111', // Use um CPF que já exista
},
};
export const listPatientsTestData = {
success: { fullName: 'Silva', limit: 5 },
noFilter: {},
};
// --- Agendamentos ---
export const slotsTestData = {
success: { doctorId: 'uuid-de-um-medico-real', date: '2025-10-25' }, // Substitua pelo UUID de um médico
error: { doctorId: 'uuid-invalido', date: '2025-10-25' },
};

194
services/api/types.ts Normal file
View File

@ -0,0 +1,194 @@
// types.ts (V3)
// --- TIPOS DE MODELO DE DADOS (Schemas) ---
export interface User {
id: string;
email: string;
created_at: string;
}
export interface AuthResponse {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
user: User;
}
export interface UserProfile {
id: string;
email: string;
full_name: string;
phone?: string;
roles: ('admin' | 'gestor' | 'medico' | 'secretaria' | 'paciente')[];
}
export interface Patient {
id: string;
full_name: string;
cpf: string;
email: string;
phone_mobile: string;
birth_date?: string;
created_by: string;
created_at: string;
updated_at: string;
}
export interface Doctor {
id: string;
full_name: string;
crm: string;
crm_uf: string;
specialty?: string;
active: boolean;
}
export interface Appointment {
id: string;
doctor_id: string;
patient_id: string;
scheduled_at: string;
duration_minutes: number;
status: 'requested' | 'confirmed' | 'completed' | 'cancelled';
created_by: string;
cancelled_at?: string;
cancellation_reason?: string;
}
export type NewAppointmentPayload = Omit<Appointment, 'id' | 'status'> & {
status?: 'requested' | 'confirmed';
};
export interface AvailableSlot {
time: string;
available: boolean;
}
export interface Report {
id: string;
order_number: string;
patient_id: string;
status: 'draft' | 'completed';
exam?: string;
requested_by?: string;
cid_code?: string;
diagnosis?: string;
conclusion?: string;
content_html?: string;
content_json?: object;
hide_date: boolean;
hide_signature: boolean;
due_at?: string;
created_by: string;
updated_by: string;
created_at: string;
updated_at: string;
}
export type ReportInput = Partial<Omit<Report, 'id' | 'order_number' | 'created_by' | 'updated_by' | 'created_at' | 'updated_at'>> & {
patient_id: string;
};
export interface DoctorAvailability {
id: string;
doctor_id: string;
weekday: 0 | 1 | 2 | 3 | 4 | 5 | 6;
start_time: string;
end_time: string;
slot_minutes: number;
appointment_type: 'presencial' | 'telemedicina';
active: boolean;
}
export interface DoctorException {
id: string;
doctor_id: string;
date: string;
kind: 'bloqueio' | 'disponibilidade_extra';
start_time?: string | null;
end_time?: string | null;
reason?: string;
}
// --- TIPOS DE RESPOSTA DA API (UNIÕES DISCRIMINADAS) ---
/**
* Erro genérico para falhas de rede (CORS, offline, etc.).
*/
export type NetworkError = { status: 'network_error'; error: Error };
/**
* Tipo base para respostas de erro da API com um corpo JSON.
*/
export type ApiErrorResponse<Status extends number, Data> = { status: Status; data: Data };
// Respostas para: authService.login
export type LoginSuccess = { status: 200; data: AuthResponse };
export type LoginError400 = ApiErrorResponse<400, { error: string; error_description: string }>;
export type LoginResponse = LoginSuccess | LoginError400 | NetworkError;
// Respostas para: authService.sendMagicLink
export type SendMagicLinkSuccess = { status: 200; data: object };
export type SendMagicLinkError429 = ApiErrorResponse<429, { message: string }>;
export type SendMagicLinkResponse = SendMagicLinkSuccess | SendMagicLinkError429 | NetworkError;
// Respostas para: authService.logout
export type LogoutSuccess = { status: 204; data: undefined };
export type LogoutError401 = ApiErrorResponse<401, { message: string }>;
export type LogoutResponse = LogoutSuccess | LogoutError401 | NetworkError;
// Respostas para: authService.getCurrentUser
export type GetCurrentUserSuccess = { status: 200; data: UserProfile };
export type GetCurrentUserError401 = ApiErrorResponse<401, { message: string }>;
export type GetCurrentUserResponse = GetCurrentUserSuccess | GetCurrentUserError401 | NetworkError;
// Respostas para: userService.requestPasswordReset
export type RequestPasswordResetSuccess = { status: 200; data: { success: boolean; message: string } };
export type RequestPasswordResetError400 = ApiErrorResponse<400, { detail: string }>;
export type RequestPasswordResetResponse = RequestPasswordResetSuccess | RequestPasswordResetError400 | NetworkError;
// Respostas para: userService.createUserWithPassword
export type CreateUserWithPasswordSuccess = { status: 201; data: { success: boolean; user: UserProfile; patient_id?: string } };
export type CreateUserWithPasswordError400 = ApiErrorResponse<400, { error: string }>;
export type CreateUserWithPasswordError403 = ApiErrorResponse<403, { error: string }>;
export type CreateUserWithPasswordResponse = CreateUserWithPasswordSuccess | CreateUserWithPasswordError400 | CreateUserWithPasswordError403 | NetworkError;
// Respostas para: userService.hardDeleteUser_DANGEROUS
export type HardDeleteUserSuccess = { status: 200; data: { success: boolean; message: string; userId: string } };
export type HardDeleteUserError400 = ApiErrorResponse<400, { error: string }>;
export type HardDeleteUserError403 = ApiErrorResponse<403, { error: string }>;
export type HardDeleteUserResponse = HardDeleteUserSuccess | HardDeleteUserError400 | HardDeleteUserError403 | NetworkError;
// Respostas para: patientService.registerPatient
export type RegisterPatientSuccess = { status: 200; data: { success: boolean; patient_id: string; message: string } };
export type RegisterPatientError400 = ApiErrorResponse<400, { error: string; code: 'VALIDATION_ERROR'; details?: any[] }>;
export type RegisterPatientError409 = ApiErrorResponse<409, { error: string; code: 'CPF_EXISTS' | 'EMAIL_EXISTS' }>;
export type RegisterPatientError429 = ApiErrorResponse<429, { error: string; code: 'RATE_LIMIT_EXCEEDED' }>;
export type RegisterPatientError500 = ApiErrorResponse<500, { error: string; code: string }>;
export type RegisterPatientResponse = RegisterPatientSuccess | RegisterPatientError400 | RegisterPatientError409 | RegisterPatientError429 | RegisterPatientError500 | NetworkError;
// Respostas para: scheduleService.getAvailableSlots
export type GetAvailableSlotsSuccess = { status: 200; data: { slots: AvailableSlot[] } };
export type GetAvailableSlotsResponse = GetAvailableSlotsSuccess | NetworkError;
// Respostas para: scheduleService.createAppointment
export type CreateAppointmentSuccess = { status: 201; data: Appointment };
export type CreateAppointmentResponse = CreateAppointmentSuccess | NetworkError;
// Respostas para: scheduleService.cancelAppointment
export type CancelAppointmentSuccess = { status: 200; data: Appointment };
export type CancelAppointmentResponse = CancelAppointmentSuccess | NetworkError;
// Respostas para: reportService.create
export type CreateReportSuccess = { status: 201; data: Report };
export type CreateReportResponse = CreateReportSuccess | NetworkError;
// Respostas para: reportService.update
export type UpdateReportSuccess = { status: 200; data: Report };
export type UpdateReportResponse = UpdateReportSuccess | NetworkError;
// Tipos genéricos para listagens simples
export type ListSuccess<T> = { status: 200; data: T[] };
export type ListResponse<T> = ListSuccess<T> | NetworkError;

View File

@ -1,37 +1,89 @@
// Caminho: services/disponibilidadeApi.ts
// Caminho: services/disponibilidadeApi.ts (Completo e Corrigido)
import api from './api';
export interface Availability {
id: any;
// --- Tipagem para Disponibilidade Semanal ---
export interface DoctorAvailability {
id: string;
doctor_id: string;
weekday: string;
weekday: 0 | 1 | 2 | 3 | 4 | 5 | 6;
start_time: string;
end_time: string;
[key: string]: any;
slot_minutes: number;
appointment_type: 'presencial' | 'telemedicina';
active: boolean;
created_at: string;
updated_at: string;
created_by: string;
}
export const disponibilidadeApi = {
list: async (): Promise<Availability[]> => {
const response = await api.get<Availability[]>('/rest/v1/doctor_availability?select=*');
return response.data;
},
export type CreateDoctorAvailabilityData = Omit<DoctorAvailability, 'id' | 'created_at' | 'updated_at'>;
export type UpdateDoctorAvailabilityData = Partial<Omit<DoctorAvailability, 'id' | 'created_at' | 'updated_at' | 'created_by'>>;
getById: async (id: string): Promise<Availability> => {
const response = await api.get<Availability>(`/rest/v1/doctor_availability?id=eq.${id}&select=*`, {
headers: { Accept: 'application/vnd.pgrst.object+json' },
interface ListAvailabilityParams {
doctor_id?: string;
weekday?: number;
active?: boolean;
}
// --- Tipagem para Exceções de Agenda ---
export interface DoctorException {
id: string;
doctor_id: string;
date: string;
kind: 'bloqueio' | 'disponibilidade_extra';
start_time: string | null;
end_time: string | null;
reason?: string;
created_at: string;
created_by: string;
}
export type CreateDoctorExceptionData = Omit<DoctorException, 'id' | 'created_at'>;
interface ListExceptionParams {
doctor_id?: string;
date?: string;
kind?: 'bloqueio' | 'disponibilidade_extra';
}
// --- Objeto do Serviço de API ---
export const disponibilidadeApi = {
// --- Métodos para Disponibilidade Semanal ---
list: async (params: ListAvailabilityParams = {}): Promise<DoctorAvailability[]> => {
// CORREÇÃO: Formata os parâmetros para o padrão do Supabase
const queryParams: { [key: string]: string } = { select: '*' };
if (params.doctor_id) queryParams.doctor_id = `eq.${params.doctor_id}`;
if (params.weekday !== undefined) queryParams.weekday = `eq.${params.weekday}`;
if (params.active !== undefined) queryParams.active = `eq.${params.active}`;
const response = await api.get<DoctorAvailability[]>('/rest/v1/doctor_availability', {
params: queryParams,
});
return response.data;
},
create: async (data: Omit<Availability, 'id'>): Promise<Availability> => {
const response = await api.post<Availability[]>('/rest/v1/doctor_availability', data, {
getById: async (id: string): Promise<DoctorAvailability> => {
const response = await api.get<DoctorAvailability[]>(`/rest/v1/doctor_availability?id=eq.${id}&select=*`);
if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Registro de disponibilidade não encontrado.");
},
create: async (data: CreateDoctorAvailabilityData): Promise<DoctorAvailability> => {
const response = await api.post<DoctorAvailability[]>('/rest/v1/doctor_availability', data, {
headers: { 'Prefer': 'return=representation' }
});
return response.data[0];
},
update: async (id: string, data: Partial<Availability>): Promise<Availability> => {
const response = await api.patch<Availability[]>(`/rest/v1/doctor_availability?id=eq.${id}`, data, {
update: async (id: string, data: UpdateDoctorAvailabilityData): Promise<DoctorAvailability> => {
const response = await api.patch<DoctorAvailability[]>(`/rest/v1/doctor_availability?id=eq.${id}`, data, {
headers: { 'Prefer': 'return=representation' }
});
return response.data[0];
@ -40,4 +92,28 @@ export const disponibilidadeApi = {
delete: async (id: string): Promise<void> => {
await api.delete(`/rest/v1/doctor_availability?id=eq.${id}`);
},
// --- Métodos para Exceções de Agenda ---
listExceptions: async (params: ListExceptionParams = {}): Promise<DoctorException[]> => {
// CORREÇÃO: Formata os parâmetros para o padrão do Supabase
const queryParams: { [key: string]: string } = { select: '*' };
if (params.doctor_id) queryParams.doctor_id = `eq.${params.doctor_id}`;
if (params.date) queryParams.date = `eq.${params.date}`;
if (params.kind) queryParams.kind = `eq.${params.kind}`;
const response = await api.get<DoctorException[]>('/rest/v1/doctor_exceptions', {
params: queryParams,
});
return response.data;
},
createException: async (data: CreateDoctorExceptionData): Promise<DoctorException> => {
const response = await api.post<DoctorException[]>('/rest/v1/doctor_exceptions', data, {
headers: {
'Prefer': 'return=representation',
},
});
return response.data[0];
},
};

View File

@ -1,41 +1,87 @@
// Caminho: services/medicosApi.ts
// Caminho: services/medicosApi.ts (Corrigido)
import api from './api';
export interface Doctor {
id: any;
id: string;
full_name: string;
crm: string;
crm_uf: string;
[key: string]: any;
specialty: string;
active?: boolean;
location?: string;
phone?: string;
}
export interface CreateDoctorData {
email: string;
full_name: string;
cpf: string;
crm: string;
crm_uf: string;
specialty?: string;
phone_mobile?: string;
}
interface ListParams {
active?: boolean;
specialty?: string;
}
export const medicosApi = {
list: async (): Promise<Doctor[]> => {
const response = await api.get<Doctor[]>('/rest/v1/doctors?select=*');
/**
* Lista médicos com filtros opcionais.
* @param params - Objeto com filtros como { active: true }
*/
list: async (params: ListParams = {}): Promise<Doctor[]> => {
// Prepara os parâmetros para o formato do PostgREST
const queryParams: { [key: string]: any } = {
select: '*',
};
if (params.active !== undefined) {
queryParams.active = `eq.${params.active}`; // CORREÇÃO: Usa 'eq.true' ou 'eq.false'
}
if (params.specialty) {
queryParams.specialty = `eq.${params.specialty}`;
}
const response = await api.get<Doctor[]>('/rest/v1/doctors', {
params: queryParams,
});
return response.data;
},
/**
* Cria um novo médico com validações através da Edge Function.
* @param data - Dados do médico a ser criado.
*/
createWithValidation: async (data: CreateDoctorData): Promise<any> => {
const response = await api.post('/functions/v1/create-doctor', data);
return response.data;
},
getById: async (id: string): Promise<Doctor> => {
const response = await api.get<Doctor>(`/rest/v1/doctors?id=eq.${id}&select=*`, {
headers: { Accept: 'application/vnd.pgrst.object+json' },
});
return response.data;
},
create: async (data: Omit<Doctor, 'id'>): Promise<Doctor> => {
const response = await api.post<Doctor[]>('/rest/v1/doctors', data, {
headers: { 'Prefer': 'return=representation' }
});
const response = await api.get<Doctor[]>(`/rest/v1/doctors?id=eq.${id}&select=*`);
if (response.data.length === 0) {
throw new Error("Médico não encontrado");
}
return response.data[0];
},
update: async (id: string, data: Partial<Doctor>): Promise<Doctor> => {
const response = await api.patch<Doctor[]>(`/rest/v1/doctors?id=eq.${id}`, data, {
headers: { 'Prefer': 'return=representation' }
headers: { 'Prefer': 'return=representation' },
});
return response.data[0];
},
delete: async (id: string): Promise<void> => {
await api.delete(`/rest/v1/doctors?id=eq.${id}`);
/**
* Busca uma lista de médicos de exemplo do endpoint de mock.
* Usado como fallback quando a API real não retorna dados.
*/
getMockDoctors: async (): Promise<Doctor[]> => {
const response = await api.get<Doctor[]>('https://mock.apidog.com/m1/1053378-0-default/rest/v1/doctors');
return Array.isArray(response.data) ? response.data : [response.data];
},
};

View File

@ -1,36 +1,79 @@
// Caminho: services/pacientesApi.ts
// Caminho: services/pacientesApi.ts (Completo e Corrigido)
import api from './api';
export interface Patient {
id: any;
full_name: string;
id: string;
nome_completo: string;
cpf: string;
email?: string;
telefone?: string;
data_nascimento?: string;
endereco?: string;
cidade?: string;
estado?: string;
cep?: string;
convenio?: string;
created_at?: string;
updated_at?: string;
[key: string]: any;
}
export const pacientesApi = {
list: async (): Promise<Patient[]> => {
const response = await api.get<Patient[]>('/rest/v1/patients?select=*');
return response.data;
},
export type CreatePatientData = Omit<Patient, 'id' | 'created_at' | 'updated_at'>;
export type UpdatePatientData = Partial<CreatePatientData>;
getById: async (id: string): Promise<Patient> => {
const response = await api.get<Patient>(`/rest/v1/patients?id=eq.${id}&select=*`, {
headers: { Accept: 'application/vnd.pgrst.object+json' },
interface ListParams {
limit?: number;
offset?: number;
order?: string;
nome_completo?: string;
cpf?: string;
}
export const pacientesApi = {
list: async (params: ListParams = {}): Promise<Patient[]> => {
const response = await api.get<Patient[]>('/rest/v1/patients', {
params: {
select: '*',
...params,
},
});
return response.data;
},
create: async (data: Omit<Patient, 'id'>): Promise<Patient> => {
/**
* Busca um único paciente pelo seu ID.
* @param id - O UUID do paciente.
*/
getById: async (id: string): Promise<Patient> => {
const response = await api.get<Patient[]>(`/rest/v1/patients`, {
params: {
id: `eq.${id}`,
select: '*',
},
// O header 'Accept' foi REMOVIDO para evitar o erro 406.
// A resposta será um array, então pegamos o primeiro item.
});
if (response.data && response.data.length > 0) {
return response.data[0];
}
throw new Error("Paciente não encontrado");
},
create: async (data: CreatePatientData): Promise<Patient> => {
const response = await api.post<Patient[]>('/rest/v1/patients', data, {
headers: { 'Prefer': 'return=representation' }
headers: {
'Prefer': 'return=representation',
}
});
return response.data[0];
},
update: async (id: string, data: Partial<Patient>): Promise<Patient> => {
update: async (id: string, data: UpdatePatientData): Promise<Patient> => {
const response = await api.patch<Patient[]>(`/rest/v1/patients?id=eq.${id}`, data, {
headers: { 'Prefer': 'return=representation' }
headers: {
'Prefer': 'return=representation',
}
});
return response.data[0];
},
@ -38,4 +81,12 @@ export const pacientesApi = {
delete: async (id: string): Promise<void> => {
await api.delete(`/rest/v1/patients?id=eq.${id}`);
},
getMockPatient: async (): Promise<Patient> => {
// Usamos uma instância separada do axios ou o `api.get` com a URL completa
// para chamar um domínio diferente.
const response = await api.get<Patient>('https://mock.apidog.com/m1/1053378-0-default/rest/v1/patients');
// Assumindo que o mock retorna um array, pegamos o primeiro item.
// Se retornar um objeto direto, seria apenas `response.data`.
return Array.isArray(response.data) ? response.data[0] : response.data;
},
};

View File

@ -1,5 +1,4 @@
// Caminho: services/usuariosApi.ts
import api from './api';
import api from "./api";
export interface UserRole {
id: any;
@ -23,22 +22,54 @@ export interface FullUserData {
export const usuariosApi = {
listRoles: async (): Promise<UserRole[]> => {
const response = await api.get<UserRole[]>('/rest/v1/user_roles');
const response = await api.get<UserRole[]>("/rest/v1/user_roles");
return response.data;
},
createUser: async (data: { email: string; full_name: string; role: string; [key: string]: any }): Promise<any> => {
const response = await api.post('/functions/v1/create-user', data);
return response.data;
createUser: async (data: {
email: string;
full_name: string;
phone?: string;
phone_mobile?: string;
role: string;
cpf?: string;
create_patient_record?: boolean;
}): Promise<any> => {
try {
const response = await api.post("/functions/v1/create-user", data, {
headers: {
"Content-Type": "application/json",
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "",
Authorization: `Bearer ${
typeof window !== "undefined"
? localStorage.getItem("supabase-access-token") ||
""
: ""
}`,
},
});
return response.data;
} catch (error: any) {
console.error("❌ Erro no createUser:", error.response?.data || error);
throw error;
}
},
getCurrentUser: async (): Promise<User> => {
const response = await api.get<User>('/auth/v1/user');
const response = await api.post<User>(
"/functions/v1/user-info",
{},
{ headers: { "Content-Type": "application/json" } }
);
return response.data;
},
getFullData: async (userId: string): Promise<FullUserData> => {
const response = await api.get<FullUserData>(`/functions/v1/user-info?user_id=${userId}`);
const response = await api.post<FullUserData>(
"/functions/v1/user-info-by-id",
{ user_id: userId },
{ headers: { "Content-Type": "application/json" } }
);
return response.data;
},
};
};