Compare commits

...

11 Commits

12 changed files with 1129 additions and 933 deletions

156
app/cadastro/page.tsx Normal file
View File

@ -0,0 +1,156 @@
import Link from "next/link"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Calendar, Clock, User, Shield, Stethoscope, Receipt, IdCard } from "lucide-react"
export default function HomePage() {
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<div className="container mx-auto px-4 py-16">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">Sistema de Consultas Médicas</h1>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
Gerencie suas consultas médicas de forma simples e eficiente
</p>
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
<Card className="hover:shadow-lg transition-shadow flex flex-col h-full">
<CardHeader className="text-center flex-shrink-0">
<User className="w-12 h-12 text-black-600 mx-auto mb-4" />
<CardTitle>Área do Paciente</CardTitle>
<CardDescription>Acesse sua área pessoal para agendar consultas e gerenciar seus dados</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-grow flex flex-col">
<div className="space-y-3 flex-grow">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Agendar consultas</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" />
<span>Ver histórico de consultas</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>Gerenciar dados pessoais</span>
</div>
</div>
<Link href="/patient/login" className="block mt-auto">
<Button className="w-full">Entrar como Paciente</Button>
</Link>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow flex flex-col h-full">
<CardHeader className="text-center flex-shrink-0">
<Shield className="w-12 h-12 text-purple-600 mx-auto mb-4" />
<CardTitle>Área da Secretária</CardTitle>
<CardDescription>Gerencie consultas, pacientes e agenda médica</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-grow flex flex-col">
<div className="space-y-3 flex-grow">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Gerenciar consultas</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>Cadastrar pacientes</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" />
<span>Controlar agenda</span>
</div>
</div>
<Link href="/secretary/login" className="block mt-auto">
<Button className="w-full bg-purple-600 hover:bg-purple-700">Entrar como Secretária</Button>
</Link>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow flex flex-col h-full">
<CardHeader className="text-center flex-shrink-0">
<Stethoscope className="w-12 h-12 text-green-600 mx-auto mb-4" />
<CardTitle>Área Médica</CardTitle>
<CardDescription>Acesso restrito para profissionais de saúde</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-grow flex flex-col">
<div className="space-y-3 flex-grow">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Gerenciar agenda</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>Ver pacientes</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" />
<span>Histórico de atendimentos</span>
</div>
</div>
<Link href="/doctor/login" className="block mt-auto">
<Button className="w-full bg-green-600 hover:bg-green-700">Entrar como Médico</Button>
</Link>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow flex flex-col h-full">
<CardHeader className="text-center flex-shrink-0">
<IdCard className="w-12 h-12 text-blue-600 mx-auto mb-4" />
<CardTitle>Área do Gestor</CardTitle>
<CardDescription>Acesso restrito para gestores e coordenadores</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-grow flex flex-col">
<div className="space-y-3 flex-grow">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Relatórios gerenciais</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>Configurações do sistema</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" />
<span>Gestão de usuários</span>
</div>
</div>
<Link href="#" className="block mt-auto">
<Button className="w-full bg-blue-600 hover:bg-blue-700">Entrar como Gestor</Button>
</Link>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow flex flex-col h-full">
<CardHeader className="text-center flex-shrink-0">
<Receipt className="w-12 h-12 text-orange-600 mx-auto mb-4" />
<CardTitle>Área de Finanças</CardTitle>
<CardDescription>Acesso restrito para profissionais do setor financeiro</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-grow flex flex-col">
<div className="space-y-3 flex-grow">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Relatórios financeiros</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>Faturamento</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" />
<span>Controle de pagamentos</span>
</div>
</div>
<Link href="#" className="block mt-auto">
<Button className="w-full bg-orange-600 hover:bg-orange-700">Entrar como Financeiro</Button>
</Link>
</CardContent>
</Card>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,112 @@
"use client";
import React, { createContext, useContext, useState, ReactNode } from 'react';
// A interface Appointment permanece a mesma
export interface Appointment {
id: string;
doctorName: string;
specialty: string;
date: string;
time: string;
location: string;
phone: string;
status: 'Agendada' | 'Realizada' | 'Cancelada';
observations?: string;
}
export interface AppointmentsContextType {
appointments: Appointment[];
addAppointment: (appointmentData: Omit<Appointment, 'id' | 'status'>) => void;
updateAppointment: (appointmentId: string, updatedData: Partial<Omit<Appointment, 'id'>>) => void;
// [NOVA FUNÇÃO] Adicionando a assinatura da função de exclusão ao nosso contrato
deleteAppointment: (appointmentId: string) => void;
}
const AppointmentsContext = createContext<AppointmentsContextType | undefined>(undefined);
// Os dados iniciais permanecem os mesmos
const initialAppointments: Appointment[] = [
{
id: '1',
doctorName: "Dr. João Silva",
specialty: "Cardiologia",
date: "2024-08-15",
time: "14:30",
status: "Agendada",
location: "Consultório A - 2º andar",
phone: "(11) 3333-4444",
observations: "Paciente relata dor no peito.",
},
{
id: '2',
doctorName: "Dra. Maria Santos",
specialty: "Dermatologia",
date: "2024-09-10",
time: "10:00",
status: "Agendada",
location: "Consultório B - 1º andar",
phone: "(11) 3333-5555",
},
{
id: '3',
doctorName: "Dr. Pedro Costa",
specialty: "Ortopedia",
date: "2024-07-08",
time: "16:00",
status: "Realizada",
location: "Consultório C - 3º andar",
phone: "(11) 3333-6666",
},
];
export function AppointmentsProvider({ children }: { children: ReactNode }) {
const [appointments, setAppointments] = useState<Appointment[]>(initialAppointments);
const addAppointment = (appointmentData: Omit<Appointment, 'id' | 'status'>) => {
const newAppointment: Appointment = {
id: Date.now().toString(),
status: 'Agendada',
...appointmentData,
};
setAppointments((prev) => [...prev, newAppointment]);
};
const updateAppointment = (appointmentId: string, updatedData: Partial<Omit<Appointment, 'id'>>) => {
setAppointments((prev) =>
prev.map((apt) =>
apt.id === appointmentId ? { ...apt, ...updatedData } : apt
)
);
};
// [NOVA FUNÇÃO] Implementando a lógica de exclusão real
const deleteAppointment = (appointmentId: string) => {
setAppointments((prev) =>
// O método 'filter' cria um novo array com todos os itens
// EXCETO aquele cujo ID corresponde ao que queremos excluir.
prev.filter((apt) => apt.id !== appointmentId)
);
};
const value = {
appointments,
addAppointment,
updateAppointment,
deleteAppointment, // Disponibilizando a nova função para os componentes
};
return (
<AppointmentsContext.Provider value={value}>
{children}
</AppointmentsContext.Provider>
);
}
export function useAppointments() {
const context = useContext(AppointmentsContext);
if (context === undefined) {
throw new Error('useAppointments deve ser usado dentro de um AppointmentsProvider');
}
return context;
}

View File

@ -3,6 +3,8 @@ import { GeistSans } from 'geist/font/sans'
import { GeistMono } from 'geist/font/mono' import { GeistMono } from 'geist/font/mono'
import { Analytics } from '@vercel/analytics/next' import { Analytics } from '@vercel/analytics/next'
import './globals.css' import './globals.css'
// [PASSO 1.2] - Importando o nosso provider
import { AppointmentsProvider } from './context/AppointmentsContext'
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'Clinic App', title: 'Clinic App',
@ -18,7 +20,10 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<body className={`font-sans ${GeistSans.variable} ${GeistMono.variable}`}> <body className={`font-sans ${GeistSans.variable} ${GeistMono.variable}`}>
{children} {/* [PASSO 1.2] - Envolvendo a aplicação com o provider */}
<AppointmentsProvider>
{children}
</AppointmentsProvider>
<Analytics /> <Analytics />
</body> </body>
</html> </html>

View File

@ -1,156 +1,115 @@
"use client";
import Link from "next/link" import Link from "next/link"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Calendar, Clock, User, Shield, Stethoscope, Receipt, IdCard } from "lucide-react"
export default function HomePage() {
export default function InicialPage() {
return ( return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100"> <div className="min-h-screen flex flex-col bg-gray-50">
<div className="container mx-auto px-4 py-16"> {}
<div className="text-center mb-12"> <div className="bg-black text-white text-sm py-2 px-6 flex justify-between">
<h1 className="text-4xl font-bold text-gray-900 mb-4">Sistema de Consultas Médicas</h1> <span> Horário: 08h00 - 21h00</span>
<p className="text-xl text-gray-600 max-w-2xl mx-auto"> <span> Email: contato@midconnecta.com</span>
Gerencie suas consultas médicas de forma simples e eficiente
</p>
</div>
<div className="grid md:grid-cols-3 gap-8 max-w-6xl mx-auto">
<Card className="hover:shadow-lg transition-shadow flex flex-col h-full">
<CardHeader className="text-center flex-shrink-0">
<User className="w-12 h-12 text-black-600 mx-auto mb-4" />
<CardTitle>Área do Paciente</CardTitle>
<CardDescription>Acesse sua área pessoal para agendar consultas e gerenciar seus dados</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-grow flex flex-col">
<div className="space-y-3 flex-grow">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Agendar consultas</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" />
<span>Ver histórico de consultas</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>Gerenciar dados pessoais</span>
</div>
</div>
<Link href="/patient/login" className="block mt-auto">
<Button className="w-full">Entrar como Paciente</Button>
</Link>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow flex flex-col h-full">
<CardHeader className="text-center flex-shrink-0">
<Shield className="w-12 h-12 text-purple-600 mx-auto mb-4" />
<CardTitle>Área da Secretária</CardTitle>
<CardDescription>Gerencie consultas, pacientes e agenda médica</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-grow flex flex-col">
<div className="space-y-3 flex-grow">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Gerenciar consultas</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>Cadastrar pacientes</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" />
<span>Controlar agenda</span>
</div>
</div>
<Link href="/secretary/login" className="block mt-auto">
<Button className="w-full bg-purple-600 hover:bg-purple-700">Entrar como Secretária</Button>
</Link>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow flex flex-col h-full">
<CardHeader className="text-center flex-shrink-0">
<Stethoscope className="w-12 h-12 text-green-600 mx-auto mb-4" />
<CardTitle>Área Médica</CardTitle>
<CardDescription>Acesso restrito para profissionais de saúde</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-grow flex flex-col">
<div className="space-y-3 flex-grow">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Gerenciar agenda</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>Ver pacientes</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" />
<span>Histórico de atendimentos</span>
</div>
</div>
<Link href="/doctor/login" className="block mt-auto">
<Button className="w-full bg-green-600 hover:bg-green-700">Entrar como Médico</Button>
</Link>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow flex flex-col h-full">
<CardHeader className="text-center flex-shrink-0">
<IdCard className="w-12 h-12 text-blue-600 mx-auto mb-4" />
<CardTitle>Área do Gestor</CardTitle>
<CardDescription>Acesso restrito para gestores e coordenadores</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-grow flex flex-col">
<div className="space-y-3 flex-grow">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Relatórios gerenciais</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>Configurações do sistema</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" />
<span>Gestão de usuários</span>
</div>
</div>
<Link href="/manager/login" className="block mt-auto">
<Button className="w-full bg-blue-600 hover:bg-blue-700">Entrar como Gestor</Button>
</Link>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow flex flex-col h-full">
<CardHeader className="text-center flex-shrink-0">
<Receipt className="w-12 h-12 text-orange-600 mx-auto mb-4" />
<CardTitle>Área de Finanças</CardTitle>
<CardDescription>Acesso restrito para profissionais do setor financeiro</CardDescription>
</CardHeader>
<CardContent className="space-y-4 flex-grow flex flex-col">
<div className="space-y-3 flex-grow">
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar className="w-4 h-4" />
<span>Relatórios financeiros</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<User className="w-4 h-4" />
<span>Faturamento</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Clock className="w-4 h-4" />
<span>Controle de pagamentos</span>
</div>
</div>
<Link href="/finance/login" className="block mt-auto">
<Button className="w-full bg-orange-600 hover:bg-orange-700">Entrar como Financeiro</Button>
</Link>
</CardContent>
</Card>
</div>
</div> </div>
{}
<header className="bg-white shadow-md py-4 px-6 flex justify-between items-center">
<h1 className="text-2xl font-bold text-blue-700">MidConnecta</h1>
<nav className="flex space-x-6 text-gray-700 font-medium">
<a href="#home" className="hover:text-blue-600">Home</a>
<a href="#about" className="hover:text-blue-600">Sobre</a>
<a href="#departments" className="hover:text-blue-600">Departamentos</a>
<a href="#doctors" className="hover:text-blue-600">Médicos</a>
<a href="#contact" className="hover:text-blue-600">Contato</a>
</nav>
<div className="flex space-x-4">
{}
<Link href="/cadastro">
<Button
variant="outline"
className="rounded-full px-6 py-2 border-2 border-blue-600 text-blue-600 hover:bg-blue-600 hover:text-white transition cursor-pointer"
>
Login
</Button>
</Link>
</div>
</header>
{}
<section className="flex flex-col md:flex-row items-center justify-between px-10 md:px-20 py-16 bg-gray-100">
<div className="max-w-lg">
<h2 className="text-gray-600 uppercase text-sm">Bem-vindo à Saúde Digital</h2>
<h1 className="text-4xl font-extrabold text-black leading-tight mt-2">
Soluções Médicas <br /> & Cuidados com a Saúde
</h1>
<p className="text-gray-600 mt-4">
São mais de 25 anos de experiência em serviços médicos com qualidade e confiança.
</p>
<div className="mt-6 flex space-x-4">
<Button className="rounded-full px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white shadow-lg transition">
Nossos Serviços
</Button>
<Button
variant="outline"
className="rounded-full px-6 py-2 border-2 border-blue-600 text-blue-600 hover:bg-blue-600 hover:text-white transition"
>
Saiba Mais
</Button>
</div>
</div>
<div className="mt-10 md:mt-0">
<img
src="https://cdn-icons-png.flaticon.com/512/387/387561.png"
alt="Médico"
className="w-80"
/>
</div>
</section>
{}
<section className="py-16 px-10 md:px-20 bg-white">
<h2 className="text-center text-3xl font-bold text-black">Nossos Serviços</h2>
<p className="text-center text-gray-600 mt-2">Serviços médicos que oferecemos</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 mt-10">
<div className="p-6 bg-gray-100 rounded-xl shadow hover:shadow-lg transition">
<h3 className="text-xl font-semibold text-blue-600">Clínica Geral</h3>
<p className="text-gray-600 mt-2">
Atendimento médico geral com foco na prevenção e diagnóstico.
</p>
<Button className="mt-4 rounded-full bg-blue-600 hover:bg-blue-700 text-white px-5 py-2">
Agendar
</Button>
</div>
<div className="p-6 bg-gray-100 rounded-xl shadow hover:shadow-lg transition">
<h3 className="text-xl font-semibold text-blue-600">Pediatria</h3>
<p className="text-gray-600 mt-2">
Cuidado especializado para crianças e adolescentes.
</p>
<Button className="mt-4 rounded-full bg-blue-600 hover:bg-blue-700 text-white px-5 py-2">
Agendar
</Button>
</div>
<div className="p-6 bg-gray-100 rounded-xl shadow hover:shadow-lg transition">
<h3 className="text-xl font-semibold text-blue-600">Exames</h3>
<p className="text-gray-600 mt-2">
Exames laboratoriais e de imagem com precisão e agilidade.
</p>
<Button className="mt-4 rounded-full bg-blue-600 hover:bg-blue-700 text-white px-5 py-2">
Agendar
</Button>
</div>
</div>
</section>
{}
<footer className="bg-black text-white py-6 text-center">
<p>© 2025 MidConnecta</p>
</footer>
</div> </div>
) );
} }

View File

@ -1,83 +1,46 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import Link from "next/link";
import { toast } from "sonner";
import { useAppointments, Appointment } from "../../context/AppointmentsContext";
// Componentes de UI e Ícones
import PatientLayout from "@/components/patient-layout"; import PatientLayout from "@/components/patient-layout";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogClose } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Calendar, Clock, MapPin, Phone, CalendarDays, X } from "lucide-react"; import { Calendar, Clock, MapPin, Phone, CalendarDays, X, Trash2 } from "lucide-react";
import { toast } from "sonner";
import Link from "next/link";
export default function PatientAppointments() { export default function PatientAppointmentsPage() {
const [appointments, setAppointments] = useState([ const { appointments, updateAppointment, deleteAppointment } = useAppointments();
{
id: 1,
doctor: "Dr. João Silva",
specialty: "Cardiologia",
date: "2024-01-15",
time: "14:30",
status: "agendada",
location: "Consultório A - 2º andar",
phone: "(11) 3333-4444",
},
{
id: 2,
doctor: "Dra. Maria Santos",
specialty: "Dermatologia",
date: "2024-01-22",
time: "10:00",
status: "agendada",
location: "Consultório B - 1º andar",
phone: "(11) 3333-5555",
},
{
id: 3,
doctor: "Dr. Pedro Costa",
specialty: "Ortopedia",
date: "2024-01-08",
time: "16:00",
status: "realizada",
location: "Consultório C - 3º andar",
phone: "(11) 3333-6666",
},
{
id: 4,
doctor: "Dra. Ana Lima",
specialty: "Ginecologia",
date: "2024-01-05",
time: "09:30",
status: "realizada",
location: "Consultório D - 2º andar",
phone: "(11) 3333-7777",
},
]);
const [rescheduleModal, setRescheduleModal] = useState(false); // Estados para controlar os modais e os dados do formulário
const [cancelModal, setCancelModal] = useState(false); const [isRescheduleModalOpen, setRescheduleModalOpen] = useState(false);
const [selectedAppointment, setSelectedAppointment] = useState<any>(null); const [isCancelModalOpen, setCancelModalOpen] = useState(false);
const [rescheduleData, setRescheduleData] = useState({ const [selectedAppointment, setSelectedAppointment] = useState<Appointment | null>(null);
date: "",
time: "", const [rescheduleData, setRescheduleData] = useState({ date: "", time: "", reason: "" });
reason: "",
});
const [cancelReason, setCancelReason] = useState(""); const [cancelReason, setCancelReason] = useState("");
const handleReschedule = (appointment: any) => { // --- MANIPULADORES DE EVENTOS ---
const handleRescheduleClick = (appointment: Appointment) => {
setSelectedAppointment(appointment); setSelectedAppointment(appointment);
setRescheduleData({ date: "", time: "", reason: "" }); // Preenche o formulário com os dados atuais da consulta
setRescheduleModal(true); setRescheduleData({ date: appointment.date, time: appointment.time, reason: appointment.observations || "" });
setRescheduleModalOpen(true);
}; };
const handleCancel = (appointment: any) => { const handleCancelClick = (appointment: Appointment) => {
setSelectedAppointment(appointment); setSelectedAppointment(appointment);
setCancelReason(""); setCancelReason(""); // Limpa o motivo ao abrir
setCancelModal(true); setCancelModalOpen(true);
}; };
const confirmReschedule = () => { const confirmReschedule = () => {
@ -85,55 +48,78 @@ export default function PatientAppointments() {
toast.error("Por favor, selecione uma nova data e horário"); toast.error("Por favor, selecione uma nova data e horário");
return; return;
} }
if (selectedAppointment) {
setAppointments((prev) => prev.map((apt) => (apt.id === selectedAppointment.id ? { ...apt, date: rescheduleData.date, time: rescheduleData.time } : apt))); updateAppointment(selectedAppointment.id, {
date: rescheduleData.date,
setRescheduleModal(false); time: rescheduleData.time,
toast.success("Consulta reagendada com sucesso!"); observations: rescheduleData.reason, // Atualiza as observações com o motivo
});
toast.success("Consulta reagendada com sucesso!");
setRescheduleModalOpen(false);
}
}; };
const confirmCancel = () => { const confirmCancel = () => {
if (!cancelReason.trim()) {
toast.error("O motivo do cancelamento é obrigatório");
return;
}
if (cancelReason.trim().length < 10) { if (cancelReason.trim().length < 10) {
toast.error("Por favor, forneça um motivo mais detalhado (mínimo 10 caracteres)"); toast.error("Por favor, forneça um motivo com pelo menos 10 caracteres.");
return; return;
} }
if (selectedAppointment) {
setAppointments((prev) => prev.map((apt) => (apt.id === selectedAppointment.id ? { ...apt, status: "cancelada" } : apt))); // Apenas atualiza o status e adiciona o motivo do cancelamento nas observações
updateAppointment(selectedAppointment.id, {
setCancelModal(false); status: "Cancelada",
toast.success("Consulta cancelada com sucesso!"); observations: `Motivo do cancelamento: ${cancelReason}`
}; });
toast.success("Consulta cancelada com sucesso!");
const getStatusBadge = (status: string) => { setCancelModalOpen(false);
switch (status) {
case "agendada":
return <Badge className="bg-blue-100 text-blue-800">Agendada</Badge>;
case "realizada":
return <Badge className="bg-green-100 text-green-800">Realizada</Badge>;
case "cancelada":
return <Badge className="bg-red-100 text-red-800">Cancelada</Badge>;
default:
return <Badge variant="secondary">{status}</Badge>;
} }
}; };
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 handleDeleteClick = (appointmentId: string) => {
if (window.confirm("Tem certeza que deseja excluir permanentemente esta consulta? Esta ação não pode ser desfeita.")) {
deleteAppointment(appointmentId);
toast.success("Consulta excluída do histórico.");
}
};
// --- LÓGICA AUXILIAR ---
const getStatusBadge = (status: Appointment['status']) => {
switch (status) {
case "Agendada": return <Badge className="bg-blue-100 text-blue-800 font-medium">Agendada</Badge>;
case "Realizada": return <Badge className="bg-green-100 text-green-800 font-medium">Realizada</Badge>;
case "Cancelada": return <Badge className="bg-red-100 text-red-800 font-medium">Cancelada</Badge>;
}
};
const timeSlots = ["08:00", "08:30", "09:00", "09:30", "10:00", "10:30", "14:00", "14:30", "15:00", "15:30"];
const today = new Date();
today.setHours(0, 0, 0, 0); // Zera o horário para comparar apenas o dia
// ETAPA 1: ORDENAÇÃO DAS CONSULTAS
// Cria uma cópia do array e o ordena
const sortedAppointments = [...appointments].sort((a, b) => {
const statusWeight = { 'Agendada': 1, 'Realizada': 2, 'Cancelada': 3 };
// Primeiro, ordena por status (Agendada vem primeiro)
if (statusWeight[a.status] !== statusWeight[b.status]) {
return statusWeight[a.status] - statusWeight[b.status];
}
// Se o status for o mesmo, ordena por data (mais recente/futura no topo)
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
return ( return (
<PatientLayout> <PatientLayout>
<div className="space-y-6"> <div className="space-y-8">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Minhas Consultas</h1> <h1 className="text-3xl font-bold text-gray-900">Minhas Consultas</h1>
<p className="text-gray-600">Histórico e consultas agendadas</p> <p className="text-gray-600">Histórico e consultas agendadas</p>
</div> </div>
<Link href="/patient/schedule"> <Link href="/patient/schedule">
<Button> <Button className="bg-gray-800 hover:bg-gray-900 text-white">
<Calendar className="mr-2 h-4 w-4" /> <Calendar className="mr-2 h-4 w-4" />
Agendar Nova Consulta Agendar Nova Consulta
</Button> </Button>
@ -141,121 +127,159 @@ export default function PatientAppointments() {
</div> </div>
<div className="grid gap-6"> <div className="grid gap-6">
{appointments.map((appointment) => ( {/* Utiliza o array ORDENADO para a renderização */}
<Card key={appointment.id}> {sortedAppointments.map((appointment) => {
<CardHeader> const appointmentDate = new Date(appointment.date);
<div className="flex justify-between items-start"> let displayStatus = appointment.status;
<div>
<CardTitle className="text-lg">{appointment.doctor}</CardTitle>
<CardDescription>{appointment.specialty}</CardDescription>
</div>
{getStatusBadge(appointment.status)}
</div>
</CardHeader>
<CardContent>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-3">
<div className="flex items-center text-sm text-gray-600">
<Calendar className="mr-2 h-4 w-4" />
{new Date(appointment.date).toLocaleDateString("pt-BR")}
</div>
<div className="flex items-center text-sm text-gray-600">
<Clock className="mr-2 h-4 w-4" />
{appointment.time}
</div>
</div>
<div className="space-y-3">
<div className="flex items-center text-sm text-gray-600">
<MapPin className="mr-2 h-4 w-4" />
{appointment.location}
</div>
<div className="flex items-center text-sm text-gray-600">
<Phone className="mr-2 h-4 w-4" />
{appointment.phone}
</div>
</div>
</div>
{appointment.status === "agendada" && ( if (appointment.status === 'Agendada' && appointmentDate < today) {
<div className="flex gap-2 mt-4 pt-4 border-t"> displayStatus = 'Realizada';
<Button variant="outline" size="sm" onClick={() => handleReschedule(appointment)}> }
return (
<Card key={appointment.id} className="overflow-hidden">
<CardHeader>
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-xl">{appointment.doctorName}</CardTitle>
<CardDescription>{appointment.specialty}</CardDescription>
</div>
{getStatusBadge(displayStatus)}
</div>
</CardHeader>
<CardContent>
<div className="grid md:grid-cols-2 gap-x-8 gap-y-4 mb-6">
<div className="space-y-4">
<div className="flex items-center text-sm text-gray-700">
<Calendar className="mr-3 h-4 w-4 text-gray-500" />
{new Date(appointment.date).toLocaleDateString("pt-BR", { timeZone: 'UTC' })}
</div>
<div className="flex items-center text-sm text-gray-700">
<Clock className="mr-3 h-4 w-4 text-gray-500" />
{appointment.time}
</div>
</div>
<div className="space-y-4">
<div className="flex items-center text-sm text-gray-700">
<MapPin className="mr-3 h-4 w-4 text-gray-500" />
{appointment.location || 'Local não informado'}
</div>
<div className="flex items-center text-sm text-gray-700">
<Phone className="mr-3 h-4 w-4 text-gray-500" />
{appointment.phone || 'Telefone não informado'}
</div>
</div>
</div>
{/* Container ÚNICO para todas as ações */}
<div className="flex gap-2 pt-4 border-t">
{(displayStatus === "Agendada") && (
<>
<Button variant="outline" size="sm" onClick={() => handleRescheduleClick(appointment)}>
<CalendarDays className="mr-2 h-4 w-4" /> <CalendarDays className="mr-2 h-4 w-4" />
Reagendar Reagendar
</Button> </Button>
<Button variant="outline" size="sm" className="text-red-600 hover:text-red-700 hover:bg-red-50 bg-transparent" onClick={() => handleCancel(appointment)}> <Button variant="ghost" size="sm" className="text-orange-600 hover:text-orange-700 hover:bg-orange-50" onClick={() => handleCancelClick(appointment)}>
<X className="mr-2 h-4 w-4" /> <X className="mr-2 h-4 w-4" />
Cancelar Cancelar
</Button>
</>
)}
{(displayStatus === "Realizada" || displayStatus === "Cancelada") && (
<Button variant="ghost" size="sm" className="text-red-600 hover:text-red-700 hover:bg-red-50" onClick={() => handleDeleteClick(appointment.id)}>
<Trash2 className="mr-2 h-4 w-4" />
Excluir do Histórico
</Button> </Button>
)}
</div> </div>
)} </CardContent>
</CardContent> </Card>
</Card> );
))} })}
</div> </div>
</div> </div>
<Dialog open={rescheduleModal} onOpenChange={setRescheduleModal}> {/* ETAPA 2: CONSTRUÇÃO DOS MODAIS */}
<DialogContent className="sm:max-w-[425px]">
{/* Modal de Reagendamento */}
<Dialog open={isRescheduleModalOpen} onOpenChange={setRescheduleModalOpen}>
<DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Reagendar Consulta</DialogTitle> <DialogTitle>Reagendar Consulta</DialogTitle>
<DialogDescription>Reagendar consulta com {selectedAppointment?.doctor}</DialogDescription> <DialogDescription>
Reagendar consulta com {selectedAppointment?.doctorName}.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="grid gap-4 py-4">
<div className="grid gap-2"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="date">Nova Data</Label> <Label htmlFor="date" className="text-right">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]} /> <Input
id="date"
type="date"
value={rescheduleData.date}
onChange={(e) => setRescheduleData({...rescheduleData, date: e.target.value})}
className="col-span-3"
/>
</div> </div>
<div className="grid gap-2"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="time">Novo Horário</Label> <Label htmlFor="time" className="text-right">Novo Horário</Label>
<Select value={rescheduleData.time} onValueChange={(value) => setRescheduleData((prev) => ({ ...prev, time: value }))}> <Select
<SelectTrigger> value={rescheduleData.time}
onValueChange={(value) => setRescheduleData({...rescheduleData, time: value})}
>
<SelectTrigger className="col-span-3">
<SelectValue placeholder="Selecione um horário" /> <SelectValue placeholder="Selecione um horário" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{timeSlots.map((time) => ( {timeSlots.map(time => <SelectItem key={time} value={time}>{time}</SelectItem>)}
<SelectItem key={time} value={time}>
{time}
</SelectItem>
))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="grid gap-2"> <div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="reason">Motivo do Reagendamento (opcional)</Label> <Label htmlFor="reason" className="text-right">Motivo</Label>
<Textarea id="reason" placeholder="Informe o motivo do reagendamento..." value={rescheduleData.reason} onChange={(e) => setRescheduleData((prev) => ({ ...prev, reason: e.target.value }))} /> <Textarea
id="reason"
placeholder="Informe o motivo do reagendamento (opcional)"
value={rescheduleData.reason}
onChange={(e) => setRescheduleData({...rescheduleData, reason: e.target.value})}
className="col-span-3"
/>
</div> </div>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setRescheduleModal(false)}> <DialogClose asChild>
Cancelar <Button type="button" variant="outline">Cancelar</Button>
</Button> </DialogClose>
<Button onClick={confirmReschedule}>Confirmar Reagendamento</Button> <Button type="button" onClick={confirmReschedule}>Confirmar Reagendamento</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={cancelModal} onOpenChange={setCancelModal}> {/* Modal de Cancelamento */}
<DialogContent className="sm:max-w-[425px]"> <Dialog open={isCancelModalOpen} onOpenChange={setCancelModalOpen}>
<DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Cancelar Consulta</DialogTitle> <DialogTitle>Cancelar Consulta</DialogTitle>
<DialogDescription>Tem certeza que deseja cancelar a consulta com {selectedAppointment?.doctor}?</DialogDescription> <DialogDescription>
Você tem certeza que deseja cancelar sua consulta com {selectedAppointment?.doctorName}? Esta ação não pode ser desfeita.
</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="grid gap-4 py-4"> <div className="py-4">
<div className="grid gap-2"> <Label htmlFor="cancelReason">Motivo do Cancelamento (obrigatório)</Label>
<Label htmlFor="cancel-reason" className="text-sm font-medium"> <Textarea
Motivo do Cancelamento <span className="text-red-500">*</span> id="cancelReason"
</Label> placeholder="Por favor, descreva o motivo do cancelamento..."
<Textarea id="cancel-reason" placeholder="Por favor, informe o motivo do cancelamento... (obrigatório)" value={cancelReason} onChange={(e) => setCancelReason(e.target.value)} required className={`min-h-[100px] ${!cancelReason.trim() && cancelModal ? "border-red-300 focus:border-red-500" : ""}`} /> value={cancelReason}
<p className="text-xs text-gray-500">Mínimo de 10 caracteres. Este campo é obrigatório.</p> onChange={(e) => setCancelReason(e.target.value)}
</div> className="mt-2"
/>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="outline" onClick={() => setCancelModal(false)}> <DialogClose asChild>
Voltar <Button type="button" variant="outline">Voltar</Button>
</Button> </DialogClose>
<Button variant="destructive" onClick={confirmCancel} disabled={!cancelReason.trim() || cancelReason.trim().length < 10}> <Button type="button" variant="destructive" onClick={confirmCancel}>Confirmar Cancelamento</Button>
Confirmar Cancelamento
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -1,50 +1,102 @@
"use client" "use client";
import type React from "react" import type React from "react";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { useState } from "react" // [SINCRONIZAÇÃO 1] - Importando a lista de 'appointments' para a validação de conflito
import PatientLayout from "@/components/patient-layout" import { useAppointments } from "../../context/AppointmentsContext";
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 { Calendar, Clock, User } from "lucide-react"
export default function ScheduleAppointment() { // Componentes de UI e Layout
const [selectedDoctor, setSelectedDoctor] = useState("") import PatientLayout from "@/components/patient-layout";
const [selectedDate, setSelectedDate] = useState("") import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
const [selectedTime, setSelectedTime] = useState("") import { Button } from "@/components/ui/button";
const [notes, setNotes] = useState("") 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 { Calendar, Clock, User } from "lucide-react";
const doctors = [ // Interface para o estado local do formulário (sem alterações)
{ id: "1", name: "Dr. João Silva", specialty: "Cardiologia" }, interface AppointmentFormState {
{ id: "2", name: "Dra. Maria Santos", specialty: "Dermatologia" }, doctorId: string;
{ id: "3", name: "Dr. Pedro Costa", specialty: "Ortopedia" }, date: string;
{ id: "4", name: "Dra. Ana Lima", specialty: "Ginecologia" }, time: string;
] observations: string;
}
const availableTimes = [ // --- DADOS MOCKADOS (ALTERAÇÃO 1: Adicionando location e phone) ---
"08:00", const doctors = [
"08:30", { id: "1", name: "Dr. João Silva", specialty: "Cardiologia", location: "Consultório A - 2º andar", phone: "(11) 3333-4444" },
"09:00", { id: "2", name: "Dra. Maria Santos", specialty: "Dermatologia", location: "Consultório B - 1º andar", phone: "(11) 3333-5555" },
"09:30", { id: "3", name: "Dr. Pedro Costa", specialty: "Ortopedia", location: "Consultório C - 3º andar", phone: "(11) 3333-6666" },
"10:00", ];
"10:30", const availableTimes = ["09:00", "09:30", "10:00", "10:30", "14:00", "14:30", "15:00"];
"14:00", // -------------------------------------------------------------
"14:30",
"15:00", export default function ScheduleAppointmentPage() {
"15:30", const router = useRouter();
"16:00", // [SINCRONIZAÇÃO 1 - continuação] - Obtendo a lista de agendamentos existentes
"16:30", const { addAppointment, appointments } = useAppointments();
]
const [formData, setFormData] = useState<AppointmentFormState>({
doctorId: "",
date: "",
time: "",
observations: "",
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setFormData(prevState => ({ ...prevState, [name]: value }));
};
const handleSelectChange = (name: keyof AppointmentFormState, value: string) => {
setFormData(prevState => ({ ...prevState, [name]: value }));
};
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault();
// Aqui você implementaria a lógica para salvar o agendamento if (!formData.doctorId || !formData.date || !formData.time) {
alert("Consulta agendada com sucesso!") toast.error("Por favor, preencha os campos de médico, data e horário.");
} return;
}
const selectedDoctor = doctors.find(doc => doc.id === formData.doctorId);
if (!selectedDoctor) return;
// Validação de conflito (sem alterações, já estava correta)
const isConflict = appointments.some(
(apt) =>
apt.doctorName === selectedDoctor.name &&
apt.date === formData.date &&
apt.time === formData.time
);
if (isConflict) {
toast.error("Este horário já está ocupado para o médico selecionado.");
return;
}
// [ALTERAÇÃO 2] - Utilizando os dados do médico selecionado para location e phone
// e removendo os placeholders.
addAppointment({
doctorName: selectedDoctor.name,
specialty: selectedDoctor.specialty,
date: formData.date,
time: formData.time,
observations: formData.observations,
location: selectedDoctor.location, // Usando a localização do médico
phone: selectedDoctor.phone, // Usando o telefone do médico
});
toast.success("Consulta agendada com sucesso!");
router.push('/patient/appointments');
};
// Validação de data passada (sem alterações, já estava correta)
const today = new Date().toISOString().split('T')[0];
return ( return (
<PatientLayout> <PatientLayout>
@ -54,7 +106,7 @@ export default function ScheduleAppointment() {
<p className="text-gray-600">Escolha o médico, data e horário para sua consulta</p> <p className="text-gray-600">Escolha o médico, data e horário para sua consulta</p>
</div> </div>
<div className="grid lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<Card> <Card>
<CardHeader> <CardHeader>
@ -65,9 +117,12 @@ export default function ScheduleAppointment() {
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="doctor">Médico</Label> <Label htmlFor="doctor">Médico</Label>
<Select value={selectedDoctor} onValueChange={setSelectedDoctor}> <Select
value={formData.doctorId}
onValueChange={(value) => handleSelectChange('doctorId', value)}
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione um médico" /> <SelectValue placeholder="Seleione um médico" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{doctors.map((doctor) => ( {doctors.map((doctor) => (
@ -79,21 +134,24 @@ export default function ScheduleAppointment() {
</Select> </Select>
</div> </div>
<div className="grid md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="date">Data</Label> <Label htmlFor="date">Data</Label>
<Input <Input
id="date" id="date"
name="date"
type="date" type="date"
value={selectedDate} value={formData.date}
onChange={(e) => setSelectedDate(e.target.value)} onChange={handleChange}
min={new Date().toISOString().split("T")[0]} min={today}
/> />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="time">Horário</Label> <Label htmlFor="time">Horário</Label>
<Select value={selectedTime} onValueChange={setSelectedTime}> <Select
value={formData.time}
onValueChange={(value) => handleSelectChange('time', value)}
>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione um horário" /> <SelectValue placeholder="Selecione um horário" />
</SelectTrigger> </SelectTrigger>
@ -109,17 +167,18 @@ export default function ScheduleAppointment() {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="notes">Observações (opcional)</Label> <Label htmlFor="observations">Observações (opcional)</Label>
<Textarea <Textarea
id="notes" id="observations"
name="observations"
placeholder="Descreva brevemente o motivo da consulta ou observações importantes" placeholder="Descreva brevemente o motivo da consulta ou observações importantes"
value={notes} value={formData.observations}
onChange={(e) => setNotes(e.target.value)} onChange={handleChange}
rows={3} rows={4}
/> />
</div> </div>
<Button type="submit" className="w-full" disabled={!selectedDoctor || !selectedDate || !selectedTime}> <Button type="submit" className="w-full bg-gray-600 hover:bg-gray-700 text-white text-base py-6">
Agendar Consulta Agendar Consulta
</Button> </Button>
</form> </form>
@ -130,30 +189,30 @@ export default function ScheduleAppointment() {
<div className="space-y-6"> <div className="space-y-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center"> <CardTitle className="flex items-center text-base">
<Calendar className="mr-2 h-5 w-5" /> <Calendar className="mr-2 h-5 w-5" />
Resumo Resumo
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-3 text-sm">
{selectedDoctor && ( {formData.doctorId ? (
<div className="flex items-center space-x-2"> <div className="flex items-center">
<User className="h-4 w-4 text-gray-500" /> <User className="mr-2 h-4 w-4 text-gray-500" />
<span className="text-sm">{doctors.find((d) => d.id === selectedDoctor)?.name}</span> <span>{doctors.find((d) => d.id === formData.doctorId)?.name}</span>
</div>
) : <p className="text-gray-500">Preencha o formulário...</p>}
{formData.date && (
<div className="flex items-center">
<Calendar className="mr-2 h-4 w-4 text-gray-500" />
<span>{new Date(formData.date).toLocaleDateString("pt-BR", { timeZone: 'UTC' })}</span>
</div> </div>
)} )}
{selectedDate && ( {formData.time && (
<div className="flex items-center space-x-2"> <div className="flex items-center">
<Calendar className="h-4 w-4 text-gray-500" /> <Clock className="mr-2 h-4 w-4 text-gray-500" />
<span className="text-sm">{new Date(selectedDate).toLocaleDateString("pt-BR")}</span> <span>{formData.time}</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> </div>
)} )}
</CardContent> </CardContent>
@ -161,18 +220,20 @@ export default function ScheduleAppointment() {
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Informações Importantes</CardTitle> <CardTitle className="text-base">Informações Importantes</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="text-sm text-gray-600 space-y-2"> <CardContent>
<p> Chegue com 15 minutos de antecedência</p> <ul className="space-y-2 text-sm text-gray-600 list-disc list-inside">
<p> Traga documento com foto</p> <li>Chegue com 15 minutos de antecedência</li>
<p> Traga carteirinha do convênio</p> <li>Traga documento com foto</li>
<p> Traga exames anteriores, se houver</p> <li>Traga carteirinha do convênio</li>
<li>Traga exames anteriores, se houver</li>
</ul>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
</PatientLayout> </PatientLayout>
) );
} }

View File

@ -14,56 +14,13 @@ import { ArrowLeft, Save, Trash2, Paperclip, Upload } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import SecretaryLayout from "@/components/secretary-layout"; import SecretaryLayout from "@/components/secretary-layout";
import { patientsService } from "@/services/patientsApi.mjs";
// Mock data - in a real app, this would come from an API import { json } from "stream/consumers";
const mockPatients = [
{
id: 1,
nome: "Aaron Avalos Perez",
cpf: "123.456.789-00",
rg: "12.345.678-9",
sexo: "masculino",
dataNascimento: "1990-01-15",
etnia: "branca",
raca: "caucasiana",
naturalidade: "Aracaju",
nacionalidade: "brasileira",
profissao: "Engenheiro",
estadoCivil: "solteiro",
nomeMae: "Maria Perez",
profissaoMae: "Professora",
nomePai: "João Perez",
profissaoPai: "Médico",
nomeResponsavel: "",
cpfResponsavel: "",
nomeEsposo: "",
email: "aaron@email.com",
celular: "(79) 99943-2499",
telefone1: "(79) 3214-5678",
telefone2: "",
cep: "49000-000",
endereco: "Rua das Flores, 123",
numero: "123",
complemento: "Apt 101",
bairro: "Centro",
cidade: "Aracaju",
estado: "SE",
tipoSanguineo: "O+",
peso: "75",
altura: "1.75",
alergias: "Nenhuma alergia conhecida",
convenio: "Particular",
plano: "Premium",
numeroMatricula: "123456789",
validadeCarteira: "2025-12-31",
observacoes: "Paciente colaborativo",
},
];
export default function EditarPacientePage() { export default function EditarPacientePage() {
const router = useRouter(); const router = useRouter();
const params = useParams(); const params = useParams();
const patientId = Number.parseInt(params.id as string); const patientId = params.id;
const { toast } = useToast(); const { toast } = useToast();
// Photo upload state // Photo upload state
@ -75,45 +32,107 @@ export default function EditarPacientePage() {
const [isUploadingAnexo, setIsUploadingAnexo] = useState(false); const [isUploadingAnexo, setIsUploadingAnexo] = useState(false);
const anexoInputRef = useRef<HTMLInputElement | null>(null); const anexoInputRef = useRef<HTMLInputElement | null>(null);
const [formData, setFormData] = useState({ type FormData = {
nome: string; // full_name
cpf: string;
dataNascimento: string; // birth_date
sexo: string; // sex
id?: string;
nomeSocial?: string; // social_name
rg?: string;
documentType?: string; // document_type
documentNumber?: string; // document_number
ethnicity?: string;
race?: string;
naturality?: string;
nationality?: string;
profession?: string;
maritalStatus?: string; // marital_status
motherName?: string; // mother_name
motherProfession?: string; // mother_profession
fatherName?: string; // father_name
fatherProfession?: string; // father_profession
guardianName?: string; // guardian_name
guardianCpf?: string; // guardian_cpf
spouseName?: string; // spouse_name
rnInInsurance?: boolean; // rn_in_insurance
legacyCode?: string; // legacy_code
notes?: string;
email?: string;
phoneMobile?: string; // phone_mobile
phone1?: string;
phone2?: string;
cep?: string;
street?: string;
number?: string;
complement?: string;
neighborhood?: string;
city?: string;
state?: string;
reference?: string;
vip?: boolean;
lastVisitAt?: string;
nextAppointmentAt?: string;
createdAt?: string;
updatedAt?: string;
createdBy?: string;
updatedBy?: string;
weightKg?: string;
heightM?: string;
bmi?: string;
bloodType?: string;
};
const [formData, setFormData] = useState<FormData>({
nome: "", nome: "",
cpf: "", cpf: "",
rg: "",
sexo: "",
dataNascimento: "", dataNascimento: "",
etnia: "", sexo: "",
raca: "", id: "",
naturalidade: "", nomeSocial: "",
nacionalidade: "", rg: "",
profissao: "", documentType: "",
estadoCivil: "", documentNumber: "",
nomeMae: "", ethnicity: "",
profissaoMae: "", race: "",
nomePai: "", naturality: "",
profissaoPai: "", nationality: "",
nomeResponsavel: "", profession: "",
cpfResponsavel: "", maritalStatus: "",
nomeEsposo: "", motherName: "",
motherProfession: "",
fatherName: "",
fatherProfession: "",
guardianName: "",
guardianCpf: "",
spouseName: "",
rnInInsurance: false,
legacyCode: "",
notes: "",
email: "", email: "",
celular: "", phoneMobile: "",
telefone1: "", phone1: "",
telefone2: "", phone2: "",
cep: "", cep: "",
endereco: "", street: "",
numero: "", number: "",
complemento: "", complement: "",
bairro: "", neighborhood: "",
cidade: "", city: "",
estado: "", state: "",
tipoSanguineo: "", reference: "",
peso: "", vip: false,
altura: "", lastVisitAt: "",
alergias: "", nextAppointmentAt: "",
convenio: "", createdAt: "",
plano: "", updatedAt: "",
numeroMatricula: "", createdBy: "",
validadeCarteira: "", updatedBy: "",
observacoes: "", weightKg: "",
heightM: "",
bmi: "",
bloodType: "",
}); });
const [isGuiaConvenio, setIsGuiaConvenio] = useState(false); const [isGuiaConvenio, setIsGuiaConvenio] = useState(false);
@ -122,169 +141,66 @@ export default function EditarPacientePage() {
useEffect(() => { useEffect(() => {
async function fetchPatient() { async function fetchPatient() {
try { try {
const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientId}`); const res = await patientsService.getById(patientId);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const json = await res.json();
const p = json?.data || json;
// Map API snake_case/nested to local camelCase form // Map API snake_case/nested to local camelCase form
setFormData({ setFormData({
nome: p?.nome ?? "", id: res[0]?.id ?? "",
cpf: p?.cpf ?? "", nome: res[0]?.full_name ?? "",
rg: p?.rg ?? "", nomeSocial: res[0]?.social_name ?? "",
sexo: p?.sexo ?? "", cpf: res[0]?.cpf ?? "",
dataNascimento: p?.data_nascimento ?? p?.dataNascimento ?? "", rg: res[0]?.rg ?? "",
etnia: p?.etnia ?? "", documentType: res[0]?.document_type ?? "",
raca: p?.raca ?? "", documentNumber: res[0]?.document_number ?? "",
naturalidade: p?.naturalidade ?? "", sexo: res[0]?.sex ?? "",
nacionalidade: p?.nacionalidade ?? "", dataNascimento: res[0]?.birth_date ?? "",
profissao: p?.profissao ?? "", ethnicity: res[0]?.ethnicity ?? "",
estadoCivil: p?.estado_civil ?? p?.estadoCivil ?? "", race: res[0]?.race ?? "",
nomeMae: p?.nome_mae ?? p?.nomeMae ?? "", naturality: res[0]?.naturality ?? "",
profissaoMae: p?.profissao_mae ?? p?.profissaoMae ?? "", nationality: res[0]?.nationality ?? "",
nomePai: p?.nome_pai ?? p?.nomePai ?? "", profession: res[0]?.profession ?? "",
profissaoPai: p?.profissao_pai ?? p?.profissaoPai ?? "", maritalStatus: res[0]?.marital_status ?? "",
nomeResponsavel: p?.nome_responsavel ?? p?.nomeResponsavel ?? "", motherName: res[0]?.mother_name ?? "",
cpfResponsavel: p?.cpf_responsavel ?? p?.cpfResponsavel ?? "", motherProfession: res[0]?.mother_profession ?? "",
nomeEsposo: p?.nome_esposo ?? p?.nomeEsposo ?? "", fatherName: res[0]?.father_name ?? "",
email: p?.contato?.email ?? p?.email ?? "", fatherProfession: res[0]?.father_profession ?? "",
celular: p?.contato?.celular ?? p?.celular ?? "", guardianName: res[0]?.guardian_name ?? "",
telefone1: p?.contato?.telefone1 ?? p?.telefone1 ?? "", guardianCpf: res[0]?.guardian_cpf ?? "",
telefone2: p?.contato?.telefone2 ?? p?.telefone2 ?? "", spouseName: res[0]?.spouse_name ?? "",
cep: p?.endereco?.cep ?? p?.cep ?? "", rnInInsurance: res[0]?.rn_in_insurance ?? false,
endereco: p?.endereco?.logradouro ?? p?.endereco ?? "", legacyCode: res[0]?.legacy_code ?? "",
numero: p?.endereco?.numero ?? p?.numero ?? "", notes: res[0]?.notes ?? "",
complemento: p?.endereco?.complemento ?? p?.complemento ?? "", email: res[0]?.email ?? "",
bairro: p?.endereco?.bairro ?? p?.bairro ?? "", phoneMobile: res[0]?.phone_mobile ?? "",
cidade: p?.endereco?.cidade ?? p?.cidade ?? "", phone1: res[0]?.phone1 ?? "",
estado: p?.endereco?.estado ?? p?.estado ?? "", phone2: res[0]?.phone2 ?? "",
tipoSanguineo: p?.tipo_sanguineo ?? p?.tipoSanguineo ?? "", cep: res[0]?.cep ?? "",
peso: p?.peso ? String(p.peso) : "", street: res[0]?.street ?? "",
altura: p?.altura ? String(p.altura) : "", number: res[0]?.number ?? "",
alergias: p?.alergias ?? "", complement: res[0]?.complement ?? "",
convenio: p?.convenio ?? "", neighborhood: res[0]?.neighborhood ?? "",
plano: p?.plano ?? "", city: res[0]?.city ?? "",
numeroMatricula: p?.numero_matricula ?? p?.numeroMatricula ?? "", state: res[0]?.state ?? "",
validadeCarteira: p?.validade_carteira ?? p?.validadeCarteira ?? "", reference: res[0]?.reference ?? "",
observacoes: p?.observacoes ?? "", vip: res[0]?.vip ?? false,
lastVisitAt: res[0]?.last_visit_at ?? "",
nextAppointmentAt: res[0]?.next_appointment_at ?? "",
createdAt: res[0]?.created_at ?? "",
updatedAt: res[0]?.updated_at ?? "",
createdBy: res[0]?.created_by ?? "",
updatedBy: res[0]?.updated_by ?? "",
weightKg: res[0]?.weight_kg ? String(res[0].weight_kg) : "",
heightM: res[0]?.height_m ? String(res[0].height_m) : "",
bmi: res[0]?.bmi ? String(res[0].bmi) : "",
bloodType: res[0]?.blood_type ?? "",
}); });
const foto = p?.foto_url || p?.fotoUrl;
if (foto) setPhotoUrl(foto);
} catch (e: any) { } catch (e: any) {
toast({ title: "Erro", description: e?.message || "Falha ao carregar paciente" }); toast({ title: "Erro", description: e?.message || "Falha ao carregar paciente" });
} }
} }
async function fetchAnexos() {
try {
const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientId}/anexos`);
if (!res.ok) return;
const json = await res.json();
const items = Array.isArray(json?.data) ? json.data : json;
setAnexos(Array.isArray(items) ? items : []);
} catch {}
}
fetchPatient(); fetchPatient();
fetchAnexos();
}, [patientId, toast]); }, [patientId, toast]);
const onPickPhoto = () => fileInputRef.current?.click();
const onPhotoSelected = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
setIsUploadingPhoto(true);
const form = new FormData();
// Common field name: 'foto'; also append 'file' for compatibility with some mocks
form.append("foto", file);
form.append("file", file);
const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientId}/foto`, {
method: "POST",
body: form,
});
if (!res.ok) {
throw new Error(`Falha no upload (HTTP ${res.status})`);
}
let msg = "Foto enviada com sucesso";
try {
const payload = await res.json();
if (payload?.success === false) {
throw new Error(payload?.message || "A API retornou erro");
}
if (payload?.message) msg = String(payload.message);
if (payload?.data?.foto_url || payload?.foto_url || payload?.url) {
setPhotoUrl(payload.data?.foto_url ?? payload.foto_url ?? payload.url);
}
} catch {
// Ignore JSON parse errors
}
toast({ title: "Sucesso", description: msg });
} catch (err: any) {
toast({ title: "Erro", description: err?.message || "Não foi possível enviar a foto" });
} finally {
setIsUploadingPhoto(false);
// clear the input to allow re-selecting the same file
if (fileInputRef.current) fileInputRef.current.value = "";
}
};
// Remove patient photo via API
const onRemovePhoto = async () => {
try {
const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientId}/foto`, {
method: "DELETE",
});
if (!res.ok) throw new Error(`Falha ao remover foto (HTTP ${res.status})`);
setPhotoUrl(null);
toast({ title: "Sucesso", description: "Foto removida" });
} catch (err: any) {
toast({ title: "Erro", description: err?.message || "Não foi possível remover a foto" });
}
};
// Anexos helpers
const onPickAnexo = () => anexoInputRef.current?.click();
const onAnexoSelected = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
try {
setIsUploadingAnexo(true);
const form = new FormData();
form.append("anexo", file);
form.append("file", file);
const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientId}/anexos`, {
method: "POST",
body: form,
});
if (!res.ok) throw new Error(`Falha ao enviar anexo (HTTP ${res.status})`);
// Refresh anexos list
try {
const refreshed = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientId}/anexos`);
const json = await refreshed.json();
const items = Array.isArray(json?.data) ? json.data : json;
setAnexos(Array.isArray(items) ? items : []);
} catch {}
toast({ title: "Sucesso", description: "Anexo adicionado" });
} catch (err: any) {
toast({ title: "Erro", description: err?.message || "Não foi possível enviar o anexo" });
} finally {
setIsUploadingAnexo(false);
if (anexoInputRef.current) anexoInputRef.current.value = "";
}
};
const onDeleteAnexo = async (anexoId: string | number) => {
try {
const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientId}/anexos/${anexoId}`, { method: "DELETE" });
if (!res.ok) throw new Error(`Falha ao remover anexo (HTTP ${res.status})`);
setAnexos((prev) => prev.filter((a) => String(a.id) !== String(anexoId)));
toast({ title: "Sucesso", description: "Anexo removido" });
} catch (err: any) {
toast({ title: "Erro", description: err?.message || "Não foi possível remover o anexo" });
}
};
const handleInputChange = (field: string, value: string) => { const handleInputChange = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value })); setFormData((prev) => ({ ...prev, [field]: value }));
}; };
@ -293,97 +209,43 @@ export default function EditarPacientePage() {
e.preventDefault(); e.preventDefault();
// Build API payload (snake_case) // Build API payload (snake_case)
const payload = { const payload = {
nome: formData.nome, full_name: formData.nome || null,
cpf: formData.cpf, cpf: formData.cpf || null,
rg: formData.rg || null, email: formData.email || null,
sexo: formData.sexo || null, phone_mobile: formData.phoneMobile || null,
data_nascimento: formData.dataNascimento || null, birth_date: formData.dataNascimento || null,
etnia: formData.etnia || null, social_name: formData.nomeSocial || null,
raca: formData.raca || null, sex: formData.sexo || null,
naturalidade: formData.naturalidade || null, blood_type: formData.bloodType || null,
nacionalidade: formData.nacionalidade || null, weight_kg: formData.weightKg ? Number(formData.weightKg) : null,
profissao: formData.profissao || null, height_m: formData.heightM ? Number(formData.heightM) : null,
estado_civil: formData.estadoCivil || null, street: formData.street || null,
nome_mae: formData.nomeMae || null, number: formData.number || null,
profissao_mae: formData.profissaoMae || null, complement: formData.complement || null,
nome_pai: formData.nomePai || null, neighborhood: formData.neighborhood || null,
profissao_pai: formData.profissaoPai || null, city: formData.city || null,
nome_responsavel: formData.nomeResponsavel || null, state: formData.state || null,
cpf_responsavel: formData.cpfResponsavel || null, cep: formData.cep || null,
contato: {
email: formData.email || null,
celular: formData.celular || null,
telefone1: formData.telefone1 || null,
telefone2: formData.telefone2 || null,
},
endereco: {
cep: formData.cep || null,
logradouro: formData.endereco || null,
numero: formData.numero || null,
complemento: formData.complemento || null,
bairro: formData.bairro || null,
cidade: formData.cidade || null,
estado: formData.estado || null,
},
observacoes: formData.observacoes || null,
convenio: formData.convenio || null,
plano: formData.plano || null,
numero_matricula: formData.numeroMatricula || null,
validade_carteira: formData.validadeCarteira || null,
}; };
try {
const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientId}`, {
method: "PUT",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error(`Falha ao atualizar (HTTP ${res.status})`);
toast({ title: "Sucesso", description: "Paciente atualizado com sucesso" });
router.push("/pacientes");
} catch (err: any) {
toast({ title: "Erro", description: err?.message || "Não foi possível atualizar o paciente" });
}
};
// Validate CPF on blur
const validateCpf = async (cpf: string) => {
if (!cpf) return;
try { try {
const res = await fetch("https://mock.apidog.com/m1/1053378-0-default/pacientes/validar-cpf", { await patientsService.update(patientId, payload);
method: "POST", toast({
headers: { "Content-Type": "application/json" }, title: "Sucesso",
body: JSON.stringify({ cpf }), description: "Paciente atualizado com sucesso",
variant: "default"
}); });
const json = await res.json(); router.push("/secretary/pacientes");
if (json?.success === false) {
throw new Error(json?.message || "CPF inválido");
}
if (json?.message) toast({ title: "CPF", description: String(json.message) });
} catch (err: any) { } catch (err: any) {
toast({ title: "CPF inválido", description: err?.message || "Falha na validação de CPF" }); console.error("Erro ao atualizar paciente:", err);
toast({
title: "Erro",
description: err?.message || "Não foi possível atualizar o paciente",
variant: "destructive"
});
} }
}; };
// CEP lookup on blur
const lookupCep = async (cep: string) => {
const onlyDigits = cep?.replace(/\D/g, "");
if (!onlyDigits) return;
try {
const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/utils/cep/${onlyDigits}`);
if (!res.ok) return;
const data = await res.json();
const d = data?.data || data;
setFormData((prev) => ({
...prev,
endereco: d?.logradouro ?? prev.endereco,
bairro: d?.bairro ?? prev.bairro,
cidade: d?.localidade ?? d?.cidade ?? prev.cidade,
estado: d?.uf ?? d?.estado ?? prev.estado,
complemento: d?.complemento ?? prev.complemento,
}));
} catch {}
};
return ( return (
<SecretaryLayout> <SecretaryLayout>
<div className="space-y-6"> <div className="space-y-6">
@ -403,8 +265,8 @@ export default function EditarPacientePage() {
<div className="bg-white rounded-lg border border-gray-200 p-6"> <div className="bg-white rounded-lg border border-gray-200 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-6">Anexos</h2> <h2 className="text-lg font-semibold text-gray-900 mb-6">Anexos</h2>
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<input ref={anexoInputRef} type="file" className="hidden" onChange={onAnexoSelected} /> <input ref={anexoInputRef} type="file" className="hidden" />
<Button type="button" variant="outline" onClick={onPickAnexo} disabled={isUploadingAnexo}> <Button type="button" variant="outline" disabled={isUploadingAnexo}>
<Paperclip className="w-4 h-4 mr-2" /> {isUploadingAnexo ? "Enviando..." : "Adicionar anexo"} <Paperclip className="w-4 h-4 mr-2" /> {isUploadingAnexo ? "Enviando..." : "Adicionar anexo"}
</Button> </Button>
</div> </div>
@ -418,7 +280,7 @@ export default function EditarPacientePage() {
<Paperclip className="w-4 h-4 text-gray-500 shrink-0" /> <Paperclip className="w-4 h-4 text-gray-500 shrink-0" />
<span className="text-sm text-gray-800 truncate">{a.nome || a.filename || `Anexo ${a.id}`}</span> <span className="text-sm text-gray-800 truncate">{a.nome || a.filename || `Anexo ${a.id}`}</span>
</div> </div>
<Button type="button" variant="ghost" className="text-red-600" onClick={() => onDeleteAnexo(a.id)}> <Button type="button" variant="ghost" className="text-red-600">
<Trash2 className="w-4 h-4 mr-1" /> Remover <Trash2 className="w-4 h-4 mr-1" /> Remover
</Button> </Button>
</li> </li>
@ -446,12 +308,12 @@ export default function EditarPacientePage() {
)} )}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<input ref={fileInputRef} type="file" accept="image/*" className="hidden" onChange={onPhotoSelected} /> <input ref={fileInputRef} type="file" accept="image/*" className="hidden" />
<Button type="button" variant="outline" onClick={onPickPhoto} disabled={isUploadingPhoto}> <Button type="button" variant="outline" disabled={isUploadingPhoto}>
{isUploadingPhoto ? "Enviando..." : "Enviar foto"} {isUploadingPhoto ? "Enviando..." : "Enviar foto"}
</Button> </Button>
{photoUrl && ( {photoUrl && (
<Button type="button" variant="ghost" onClick={onRemovePhoto} disabled={isUploadingPhoto}> <Button type="button" variant="ghost" disabled={isUploadingPhoto}>
Remover Remover
</Button> </Button>
)} )}
@ -465,7 +327,7 @@ export default function EditarPacientePage() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cpf">CPF *</Label> <Label htmlFor="cpf">CPF *</Label>
<Input id="cpf" value={formData.cpf} onChange={(e) => handleInputChange("cpf", e.target.value)} onBlur={() => validateCpf(formData.cpf)} placeholder="000.000.000-00" required /> <Input id="cpf" value={formData.cpf} onChange={(e) => handleInputChange("cpf", e.target.value)} placeholder="000.000.000-00" required />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
@ -477,12 +339,12 @@ export default function EditarPacientePage() {
<Label>Sexo *</Label> <Label>Sexo *</Label>
<div className="flex gap-4"> <div className="flex gap-4">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input type="radio" id="masculino" name="sexo" value="masculino" checked={formData.sexo === "masculino"} onChange={(e) => handleInputChange("sexo", e.target.value)} className="w-4 h-4 text-blue-600" /> <input type="radio" id="Masculino" name="sexo" value="Masculino" checked={formData.sexo === "Masculino"} onChange={(e) => handleInputChange("sexo", e.target.value)} className="w-4 h-4 text-blue-600" />
<Label htmlFor="masculino">Masculino</Label> <Label htmlFor="Masculino">Masculino</Label>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<input type="radio" id="feminino" name="sexo" value="feminino" checked={formData.sexo === "feminino"} onChange={(e) => handleInputChange("sexo", e.target.value)} className="w-4 h-4 text-blue-600" /> <input type="radio" id="Feminino" name="sexo" value="Feminino" checked={formData.sexo === "Feminino"} onChange={(e) => handleInputChange("sexo", e.target.value)} className="w-4 h-4 text-blue-600" />
<Label htmlFor="feminino">Feminino</Label> <Label htmlFor="Feminino">Feminino</Label>
</div> </div>
</div> </div>
</div> </div>
@ -494,7 +356,7 @@ export default function EditarPacientePage() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="etnia">Etnia</Label> <Label htmlFor="etnia">Etnia</Label>
<Select value={formData.etnia} onValueChange={(value) => handleInputChange("etnia", value)}> <Select value={formData.ethnicity} onValueChange={(value) => handleInputChange("ethnicity", value)}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione" /> <SelectValue placeholder="Selecione" />
</SelectTrigger> </SelectTrigger>
@ -510,7 +372,7 @@ export default function EditarPacientePage() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="raca">Raça</Label> <Label htmlFor="raca">Raça</Label>
<Select value={formData.raca} onValueChange={(value) => handleInputChange("raca", value)}> <Select value={formData.race} onValueChange={(value) => handleInputChange("race", value)}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione" /> <SelectValue placeholder="Selecione" />
</SelectTrigger> </SelectTrigger>
@ -524,12 +386,12 @@ export default function EditarPacientePage() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="naturalidade">Naturalidade</Label> <Label htmlFor="naturalidade">Naturalidade</Label>
<Input id="naturalidade" value={formData.naturalidade} onChange={(e) => handleInputChange("naturalidade", e.target.value)} /> <Input id="naturalidade" value={formData.naturality} onChange={(e) => handleInputChange("naturality", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="nacionalidade">Nacionalidade</Label> <Label htmlFor="nacionalidade">Nacionalidade</Label>
<Select value={formData.nacionalidade} onValueChange={(value) => handleInputChange("nacionalidade", value)}> <Select value={formData.nationality} onValueChange={(value) => handleInputChange("nationality", value)}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione" /> <SelectValue placeholder="Selecione" />
</SelectTrigger> </SelectTrigger>
@ -542,12 +404,12 @@ export default function EditarPacientePage() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="profissao">Profissão</Label> <Label htmlFor="profissao">Profissão</Label>
<Input id="profissao" value={formData.profissao} onChange={(e) => handleInputChange("profissao", e.target.value)} /> <Input id="profissao" value={formData.profession} onChange={(e) => handleInputChange("profession", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="estadoCivil">Estado civil</Label> <Label htmlFor="estadoCivil">Estado civil</Label>
<Select value={formData.estadoCivil} onValueChange={(value) => handleInputChange("estadoCivil", value)}> <Select value={formData.maritalStatus} onValueChange={(value) => handleInputChange("maritalStatus", value)}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione" /> <SelectValue placeholder="Selecione" />
</SelectTrigger> </SelectTrigger>
@ -562,37 +424,37 @@ export default function EditarPacientePage() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="nomeMae">Nome da mãe</Label> <Label htmlFor="nomeMae">Nome da mãe</Label>
<Input id="nomeMae" value={formData.nomeMae} onChange={(e) => handleInputChange("nomeMae", e.target.value)} /> <Input id="nomeMae" value={formData.motherName} onChange={(e) => handleInputChange("motherName", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="profissaoMae">Profissão da mãe</Label> <Label htmlFor="profissaoMae">Profissão da mãe</Label>
<Input id="profissaoMae" value={formData.profissaoMae} onChange={(e) => handleInputChange("profissaoMae", e.target.value)} /> <Input id="profissaoMae" value={formData.motherProfession} onChange={(e) => handleInputChange("motherProfession", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="nomePai">Nome do pai</Label> <Label htmlFor="nomePai">Nome do pai</Label>
<Input id="nomePai" value={formData.nomePai} onChange={(e) => handleInputChange("nomePai", e.target.value)} /> <Input id="nomePai" value={formData.fatherName} onChange={(e) => handleInputChange("fatherName", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="profissaoPai">Profissão do pai</Label> <Label htmlFor="profissaoPai">Profissão do pai</Label>
<Input id="profissaoPai" value={formData.profissaoPai} onChange={(e) => handleInputChange("profissaoPai", e.target.value)} /> <Input id="profissaoPai" value={formData.fatherProfession} onChange={(e) => handleInputChange("fatherProfession", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="nomeResponsavel">Nome do responsável</Label> <Label htmlFor="nomeResponsavel">Nome do responsável</Label>
<Input id="nomeResponsavel" value={formData.nomeResponsavel} onChange={(e) => handleInputChange("nomeResponsavel", e.target.value)} /> <Input id="nomeResponsavel" value={formData.guardianName} onChange={(e) => handleInputChange("guardianName", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cpfResponsavel">CPF do responsável</Label> <Label htmlFor="cpfResponsavel">CPF do responsável</Label>
<Input id="cpfResponsavel" value={formData.cpfResponsavel} onChange={(e) => handleInputChange("cpfResponsavel", e.target.value)} placeholder="000.000.000-00" /> <Input id="cpfResponsavel" value={formData.guardianCpf} onChange={(e) => handleInputChange("guardianCpf", e.target.value)} placeholder="000.000.000-00" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="nomeEsposo">Nome do esposo(a)</Label> <Label htmlFor="nomeEsposo">Nome do esposo(a)</Label>
<Input id="nomeEsposo" value={formData.nomeEsposo} onChange={(e) => handleInputChange("nomeEsposo", e.target.value)} /> <Input id="nomeEsposo" value={formData.spouseName} onChange={(e) => handleInputChange("spouseName", e.target.value)} />
</div> </div>
</div> </div>
@ -605,7 +467,7 @@ export default function EditarPacientePage() {
<div className="mt-6"> <div className="mt-6">
<Label htmlFor="observacoes">Observações</Label> <Label htmlFor="observacoes">Observações</Label>
<Textarea id="observacoes" value={formData.observacoes} onChange={(e) => handleInputChange("observacoes", e.target.value)} placeholder="Digite observações sobre o paciente..." className="mt-2" /> <Textarea id="observacoes" value={formData.notes} onChange={(e) => handleInputChange("notes", e.target.value)} placeholder="Digite observações sobre o paciente..." className="mt-2" />
</div> </div>
</div> </div>
@ -615,23 +477,23 @@ export default function EditarPacientePage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email">E-mail</Label> <Label htmlFor="email">E-mail *</Label>
<Input id="email" type="email" value={formData.email} onChange={(e) => handleInputChange("email", e.target.value)} /> <Input id="email" type="email" value={formData.email} onChange={(e) => handleInputChange("email", e.target.value)} required/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="celular">Celular</Label> <Label htmlFor="celular">Celular *</Label>
<Input id="celular" value={formData.celular} onChange={(e) => handleInputChange("celular", e.target.value)} placeholder="(00) 00000-0000" /> <Input id="celular" value={formData.phoneMobile} onChange={(e) => handleInputChange("phoneMobile", e.target.value)} placeholder="(00) 00000-0000" required/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="telefone1">Telefone 1</Label> <Label htmlFor="telefone1">Telefone 1</Label>
<Input id="telefone1" value={formData.telefone1} onChange={(e) => handleInputChange("telefone1", e.target.value)} placeholder="(00) 0000-0000" /> <Input id="telefone1" value={formData.phone1} onChange={(e) => handleInputChange("phone1", e.target.value)} placeholder="(00) 0000-0000" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="telefone2">Telefone 2</Label> <Label htmlFor="telefone2">Telefone 2</Label>
<Input id="telefone2" value={formData.telefone2} onChange={(e) => handleInputChange("telefone2", e.target.value)} placeholder="(00) 0000-0000" /> <Input id="telefone2" value={formData.phone2} onChange={(e) => handleInputChange("phone2", e.target.value)} placeholder="(00) 0000-0000" />
</div> </div>
</div> </div>
</div> </div>
@ -643,37 +505,37 @@ export default function EditarPacientePage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cep">CEP</Label> <Label htmlFor="cep">CEP</Label>
<Input id="cep" value={formData.cep} onChange={(e) => handleInputChange("cep", e.target.value)} onBlur={() => lookupCep(formData.cep)} placeholder="00000-000" /> <Input id="cep" value={formData.cep} onChange={(e) => handleInputChange("cep", e.target.value)} placeholder="00000-000" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="endereco">Endereço</Label> <Label htmlFor="endereco">Endereço</Label>
<Input id="endereco" value={formData.endereco} onChange={(e) => handleInputChange("endereco", e.target.value)} /> <Input id="endereco" value={formData.street} onChange={(e) => handleInputChange("street", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="numero">Número</Label> <Label htmlFor="numero">Número</Label>
<Input id="numero" value={formData.numero} onChange={(e) => handleInputChange("numero", e.target.value)} /> <Input id="numero" value={formData.number} onChange={(e) => handleInputChange("number", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="complemento">Complemento</Label> <Label htmlFor="complemento">Complemento</Label>
<Input id="complemento" value={formData.complemento} onChange={(e) => handleInputChange("complemento", e.target.value)} /> <Input id="complemento" value={formData.complement} onChange={(e) => handleInputChange("complement", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="bairro">Bairro</Label> <Label htmlFor="bairro">Bairro</Label>
<Input id="bairro" value={formData.bairro} onChange={(e) => handleInputChange("bairro", e.target.value)} /> <Input id="bairro" value={formData.neighborhood} onChange={(e) => handleInputChange("neighborhood", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="cidade">Cidade</Label> <Label htmlFor="cidade">Cidade</Label>
<Input id="cidade" value={formData.cidade} onChange={(e) => handleInputChange("cidade", e.target.value)} /> <Input id="cidade" value={formData.city} onChange={(e) => handleInputChange("city", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="estado">Estado</Label> <Label htmlFor="estado">Estado</Label>
<Select value={formData.estado} onValueChange={(value) => handleInputChange("estado", value)}> <Select value={formData.state} onValueChange={(value) => handleInputChange("state", value)}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione" /> <SelectValue placeholder="Selecione" />
</SelectTrigger> </SelectTrigger>
@ -718,7 +580,7 @@ export default function EditarPacientePage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="tipoSanguineo">Tipo Sanguíneo</Label> <Label htmlFor="tipoSanguineo">Tipo Sanguíneo</Label>
<Select value={formData.tipoSanguineo} onValueChange={(value) => handleInputChange("tipoSanguineo", value)}> <Select value={formData.bloodType} onValueChange={(value) => handleInputChange("bloodType", value)}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione" /> <SelectValue placeholder="Selecione" />
</SelectTrigger> </SelectTrigger>
@ -737,23 +599,23 @@ export default function EditarPacientePage() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="peso">Peso (kg)</Label> <Label htmlFor="peso">Peso (kg)</Label>
<Input id="peso" type="number" value={formData.peso} onChange={(e) => handleInputChange("peso", e.target.value)} placeholder="0.0" /> <Input id="peso" type="number" value={formData.weightKg} onChange={(e) => handleInputChange("weightKg", e.target.value)} placeholder="0.0" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="altura">Altura (m)</Label> <Label htmlFor="altura">Altura (m)</Label>
<Input id="altura" type="number" step="0.01" value={formData.altura} onChange={(e) => handleInputChange("altura", e.target.value)} placeholder="0.00" /> <Input id="altura" type="number" step="0.01" value={formData.heightM} onChange={(e) => handleInputChange("heightM", e.target.value)} placeholder="0.00" />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>IMC</Label> <Label>IMC</Label>
<Input value={formData.peso && formData.altura ? (Number.parseFloat(formData.peso) / Number.parseFloat(formData.altura) ** 2).toFixed(2) : ""} disabled placeholder="Calculado automaticamente" /> <Input value={formData.weightKg && formData.heightM ? (Number.parseFloat(formData.weightKg) / Number.parseFloat(formData.heightM) ** 2).toFixed(2) : ""} disabled placeholder="Calculado automaticamente" />
</div> </div>
</div> </div>
<div className="mt-6"> <div className="mt-6">
<Label htmlFor="alergias">Alergias</Label> <Label htmlFor="alergias">Alergias</Label>
<Textarea id="alergias" value={formData.alergias} onChange={(e) => handleInputChange("alergias", e.target.value)} placeholder="Ex: AAS, Dipirona, etc." className="mt-2" /> <Textarea id="alergias" onChange={(e) => handleInputChange("alergias", e.target.value)} placeholder="Ex: AAS, Dipirona, etc." className="mt-2" />
</div> </div>
</div> </div>
@ -764,7 +626,7 @@ export default function EditarPacientePage() {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="convenio">Convênio</Label> <Label htmlFor="convenio">Convênio</Label>
<Select value={formData.convenio} onValueChange={(value) => handleInputChange("convenio", value)}> <Select onValueChange={(value) => handleInputChange("convenio", value)}>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Selecione" /> <SelectValue placeholder="Selecione" />
</SelectTrigger> </SelectTrigger>
@ -780,17 +642,17 @@ export default function EditarPacientePage() {
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="plano">Plano</Label> <Label htmlFor="plano">Plano</Label>
<Input id="plano" value={formData.plano} onChange={(e) => handleInputChange("plano", e.target.value)} /> <Input id="plano" onChange={(e) => handleInputChange("plano", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="numeroMatricula"> de matrícula</Label> <Label htmlFor="numeroMatricula"> de matrícula</Label>
<Input id="numeroMatricula" value={formData.numeroMatricula} onChange={(e) => handleInputChange("numeroMatricula", e.target.value)} /> <Input id="numeroMatricula" onChange={(e) => handleInputChange("numeroMatricula", e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="validadeCarteira">Validade da Carteira</Label> <Label htmlFor="validadeCarteira">Validade da Carteira</Label>
<Input id="validadeCarteira" type="date" value={formData.validadeCarteira} onChange={(e) => handleInputChange("validadeCarteira", e.target.value)} disabled={validadeIndeterminada} /> <Input id="validadeCarteira" type="date" onChange={(e) => handleInputChange("validadeCarteira", e.target.value)} disabled={validadeIndeterminada} />
</div> </div>
</div> </div>
@ -803,7 +665,7 @@ export default function EditarPacientePage() {
</div> </div>
<div className="flex justify-end gap-4"> <div className="flex justify-end gap-4">
<Link href="/pacientes"> <Link href="/secretary/pacientes">
<Button type="button" variant="outline"> <Button type="button" variant="outline">
Cancelar Cancelar
</Button> </Button>

View File

@ -15,6 +15,7 @@ import { Upload, Plus, X, ChevronDown } from "lucide-react";
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { useToast } from "@/hooks/use-toast"; import { useToast } from "@/hooks/use-toast";
import SecretaryLayout from "@/components/secretary-layout"; import SecretaryLayout from "@/components/secretary-layout";
import { patientsService } from "@/services/patientsApi.mjs";
export default function NovoPacientePage() { export default function NovoPacientePage() {
const [anexosOpen, setAnexosOpen] = useState(false); const [anexosOpen, setAnexosOpen] = useState(false);
@ -31,111 +32,111 @@ export default function NovoPacientePage() {
setAnexos(anexos.filter((_, i) => i !== index)); setAnexos(anexos.filter((_, i) => i !== index));
}; };
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})/, '+55 ($1) $2-$3');
}
return cleaned.replace(/(\d{2})(\d{4})(\d{4})/, '+55 ($1) $2-$3');
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (isLoading) return; if (isLoading) return;
setIsLoading(true); setIsLoading(true);
const form = e.currentTarget; const form = e.currentTarget;
const formData = new FormData(form); const formData = new FormData(form);
const apiPayload = { const apiPayload = {
nome: formData.get("nome") as string, full_name: (formData.get("nome") as string) || "", // obrigatório
nome_social: (formData.get("nomeSocial") as string) || null, social_name: (formData.get("nomeSocial") as string) || undefined,
cpf: formData.get("cpf") as string, cpf: (formatCPF(formData.get("cpf") as string)) || "", // obrigatório
rg: (formData.get("rg") as string) || null, email: (formData.get("email") as string) || "", // obrigatório
outros_documentos: phone_mobile: (formatPhoneMobile(formData.get("celular") as string)) || "", // obrigatório
(formData.get("outrosDocumentosTipo") as string) || (formData.get("outrosDocumentosNumero") as string) birth_date: formData.get("dataNascimento") ? new Date(formData.get("dataNascimento") as string) : undefined,
? { sex: (formData.get("sexo") as string) || undefined,
tipo: (formData.get("outrosDocumentosTipo") as string) || undefined, blood_type: (formData.get("tipoSanguineo") as string) || undefined,
numero: (formData.get("outrosDocumentosNumero") as string) || undefined, weight_kg: formData.get("peso") ? parseFloat(formData.get("peso") as string) : undefined,
} height_m: formData.get("altura") ? parseFloat(formData.get("altura") as string) : undefined,
: null, cep: (formatCEP(formData.get("cep") as string)) || undefined,
sexo: (formData.get("sexo") as string) || null, street: (formData.get("endereco") as string) || undefined,
data_nascimento: (formData.get("dataNascimento") as string) || null, number: (formData.get("numero") as string) || undefined,
etnia: (formData.get("etnia") as string) || null, complement: (formData.get("complemento") as string) || undefined,
raca: (formData.get("raca") as string) || null, neighborhood: (formData.get("bairro") as string) || undefined,
naturalidade: (formData.get("naturalidade") as string) || null, city: (formData.get("cidade") as string) || undefined,
nacionalidade: (formData.get("nacionalidade") as string) || null, state: (formData.get("estado") as string) || undefined,
profissao: (formData.get("profissao") as string) || null,
estado_civil: (formData.get("estadoCivil") as string) || null,
nome_mae: (formData.get("nomeMae") as string) || null,
profissao_mae: (formData.get("profissaoMae") as string) || null,
nome_pai: (formData.get("nomePai") as string) || null,
profissao_pai: (formData.get("profissaoPai") as string) || null,
nome_responsavel: (formData.get("nomeResponsavel") as string) || null,
cpf_responsavel: (formData.get("cpfResponsavel") as string) || null,
nome_esposo: (formData.get("nomeEsposo") as string) || null,
rn_na_guia_convenio: Boolean(formData.get("rnGuia")),
codigo_legado: (formData.get("codigoLegado") as string) || null,
contato: {
email: (formData.get("email") as string) || null,
celular: (formData.get("celular") as string) || null,
telefone1: (formData.get("telefone1") as string) || null,
telefone2: (formData.get("telefone2") as string) || null,
},
endereco: {
cep: (formData.get("cep") as string) || null,
logradouro: (formData.get("endereco") as string) || null,
numero: (formData.get("numero") as string) || null,
complemento: (formData.get("complemento") as string) || null,
bairro: (formData.get("bairro") as string) || null,
cidade: (formData.get("cidade") as string) || null,
estado: (formData.get("estado") as string) || null,
referencia: null,
},
observacoes: (formData.get("observacoes") as string) || null,
// Campos de convênio (opcionais, se a API aceitar)
convenio: (formData.get("convenio") as string) || null,
plano: (formData.get("plano") as string) || null,
numero_matricula: (formData.get("numeroMatricula") as string) || null,
validade_carteira: (formData.get("validadeCarteira") as string) || null,
}; };
const errors: string[] = []; console.log(apiPayload.email)
const nome = apiPayload.nome?.trim() || ""; console.log(apiPayload.cep)
if (!nome || nome.length < 2 || nome.length > 255) errors.push("Nome deve ter entre 2 e 255 caracteres."); console.log(apiPayload.phone_mobile)
const cpf = apiPayload.cpf || "";
if (!/^\d{3}\.\d{3}\.\d{3}-\d{2}$/.test(cpf)) errors.push("CPF deve estar no formato XXX.XXX.XXX-XX.");
const sexo = apiPayload.sexo;
const allowedSexo = ["masculino", "feminino", "outro"];
if (!sexo || !allowedSexo.includes(sexo)) errors.push("Sexo é obrigatório e deve ser masculino, feminino ou outro.");
if (!apiPayload.data_nascimento) errors.push("Data de nascimento é obrigatória.");
const celular = apiPayload.contato?.celular || "";
if (celular && !/^\+55 \(\d{2}\) \d{4,5}-\d{4}$/.test(celular)) errors.push("Celular deve estar no formato +55 (XX) XXXXX-XXXX.");
const cep = apiPayload.endereco?.cep || "";
if (cep && !/^\d{5}-\d{3}$/.test(cep)) errors.push("CEP deve estar no formato XXXXX-XXX.");
const uf = apiPayload.endereco?.estado || "";
if (uf && uf.length !== 2) errors.push("Estado (UF) deve ter 2 caracteres.");
const errors: string[] = [];
const fullName = apiPayload.full_name?.trim() || "";
if (!fullName || fullName.length < 2 || fullName.length > 255) {
errors.push("Nome deve ter entre 2 e 255 caracteres.");
}
const cpf = apiPayload.cpf || "";
if (!/^\d{3}\.\d{3}\.\d{3}-\d{2}$/.test(cpf)) {
errors.push("CPF deve estar no formato XXX.XXX.XXX-XX.");
}
const sex = apiPayload.sex;
const allowedSex = ["Masculino", "Feminino", "outro"];
if (!sex || !allowedSex.includes(sex)) {
errors.push("Sexo é obrigatório e deve ser masculino, feminino ou outro.");
}
if (!apiPayload.birth_date) {
errors.push("Data de nascimento é obrigatória.");
}
const phoneMobile = apiPayload.phone_mobile || "";
if (phoneMobile && !/^\+55 \(\d{2}\) \d{4,5}-\d{4}$/.test(phoneMobile)) {
errors.push("Celular deve estar no formato +55 (XX) XXXXX-XXXX.");
}
const cep = apiPayload.cep || "";
if (cep && !/^\d{5}-\d{3}$/.test(cep)) {
errors.push("CEP deve estar no formato XXXXX-XXX.");
}
const state = apiPayload.state || "";
if (state && state.length !== 2) {
errors.push("Estado (UF) deve ter 2 caracteres.");
}
if (errors.length) { if (errors.length) {
toast({ title: "Corrija os campos", description: errors[0] }); toast({ title: "Corrija os campos", description: errors[0] });
console.log("campos errados")
setIsLoading(false); setIsLoading(false);
return; return;
} }
try { try {
const res = await fetch("https://mock.apidog.com/m1/1053378-0-default/pacientes", { const res = await patientsService.create(apiPayload);
method: "POST", console.log(res)
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(apiPayload),
});
if (!res.ok) {
const msg = `Erro ao salvar (HTTP ${res.status})`;
throw new Error(msg);
}
let message = "Paciente cadastrado com sucesso"; let message = "Paciente cadastrado com sucesso";
try { try {
const payload = await res.json(); if (!res[0].id) {
if (payload?.success === false) { throw new Error(`${res.error} ${res.message}`|| "A API retornou erro");
throw new Error(payload?.message || "A API retornou erro"); } else {
console.log(message)
} }
if (payload?.message) message = String(payload.message);
} catch {} } catch {}
toast({ toast({
@ -225,23 +226,23 @@ export default function NovoPacientePage() {
<div className="grid md:grid-cols-3 gap-4"> <div className="grid md:grid-cols-3 gap-4">
<div> <div>
<Label className="text-sm font-medium text-gray-700">Sexo</Label> <Label className="text-sm font-medium text-gray-700">Sexo *</Label>
<div className="flex gap-4 mt-2"> <div className="flex gap-4 mt-2">
<label className="flex items-center gap-2"> <label className="flex items-center gap-2">
<input type="radio" name="sexo" value="masculino" className="text-blue-600" /> <input type="radio" name="sexo" value="Masculino" className="text-blue-600" required/>
<span className="text-sm">Masculino</span> <span className="text-sm">Masculino</span>
</label> </label>
<label className="flex items-center gap-2"> <label className="flex items-center gap-2">
<input type="radio" name="sexo" value="feminino" className="text-blue-600" /> <input type="radio" name="sexo" value="Feminino" className="text-blue-600" required/>
<span className="text-sm">Feminino</span> <span className="text-sm">Feminino</span>
</label> </label>
</div> </div>
</div> </div>
<div> <div>
<Label htmlFor="dataNascimento" className="text-sm font-medium text-gray-700"> <Label htmlFor="dataNascimento" className="text-sm font-medium text-gray-700">
Data de Nascimento Data de Nascimento *
</Label> </Label>
<Input id="dataNascimento" name="dataNascimento" type="date" className="mt-1" /> <Input id="dataNascimento" name="dataNascimento" type="date" className="mt-1" required/>
</div> </div>
<div> <div>
<Label htmlFor="estadoCivil" className="text-sm font-medium text-gray-700"> <Label htmlFor="estadoCivil" className="text-sm font-medium text-gray-700">
@ -447,13 +448,13 @@ export default function NovoPacientePage() {
<div className="grid md:grid-cols-3 gap-4"> <div className="grid md:grid-cols-3 gap-4">
<div> <div>
<Label htmlFor="email" className="text-sm font-medium text-gray-700"> <Label htmlFor="email" className="text-sm font-medium text-gray-700">
E-mail E-mail *
</Label> </Label>
<Input id="email" name="email" type="email" placeholder="email@exemplo.com" className="mt-1" /> <Input id="email" name="email" type="email" placeholder="email@exemplo.com" className="mt-1" required/>
</div> </div>
<div> <div>
<Label htmlFor="celular" className="text-sm font-medium text-gray-700"> <Label htmlFor="celular" className="text-sm font-medium text-gray-700">
Celular Celular *
</Label> </Label>
<div className="flex mt-1"> <div className="flex mt-1">
<Select> <Select>
@ -464,7 +465,7 @@ export default function NovoPacientePage() {
<SelectItem value="+55">+55</SelectItem> <SelectItem value="+55">+55</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Input name="celular" placeholder="(XX) XXXXX-XXXX" className="rounded-l-none" /> <Input id="celular" name="celular" placeholder="(XX) XXXXX-XXXX" className="rounded-l-none" required/>
</div> </div>
</div> </div>
<div> <div>

View File

@ -8,6 +8,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
import { Plus, Edit, Trash2, Eye, Calendar, Filter } from "lucide-react"; import { Plus, Edit, Trash2, Eye, Calendar, Filter } from "lucide-react";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import SecretaryLayout from "@/components/secretary-layout"; import SecretaryLayout from "@/components/secretary-layout";
import { patientsService } from "@/services/patientsApi.mjs"
export default function PacientesPage() { export default function PacientesPage() {
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
@ -28,10 +29,8 @@ export default function PacientesPage() {
setDetailsDialogOpen(true); setDetailsDialogOpen(true);
setPatientDetails(null); setPatientDetails(null);
try { try {
const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes/${patientId}`); const res = await patientsService.getById(patientId);
if (!res.ok) throw new Error(`HTTP ${res.status}`); setPatientDetails(res[0]);
const json = await res.json();
setPatientDetails(json?.data ?? null);
} catch (e: any) { } catch (e: any) {
setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" }); setPatientDetails({ error: e?.message || "Erro ao buscar detalhes" });
} }
@ -43,25 +42,28 @@ export default function PacientesPage() {
setIsFetching(true); setIsFetching(true);
setError(null); setError(null);
try { try {
const res = await fetch(`https://mock.apidog.com/m1/1053378-0-default/pacientes?page=${pageToFetch}&limit=20`); const res = await patientsService.list();
if (!res.ok) throw new Error(`HTTP ${res.status}`); const mapped = res.map((p: any) => ({
const json = await res.json(); id: String(p.id ?? ""),
const items = Array.isArray(json?.data) ? json.data : []; nome: p.full_name ?? "",
const mapped = items.map((p: any) => ({ telefone: p.phone_mobile ?? p.phone1 ?? "",
id: String(p.id ?? ""), cidade: p.city ?? "",
nome: p.nome ?? "", estado: p.state ?? "",
telefone: p?.contato?.celular ?? p?.contato?.telefone1 ?? p?.telefone ?? "", ultimoAtendimento: p.last_visit_at ?? "",
cidade: p?.endereco?.cidade ?? p?.cidade ?? "", proximoAtendimento: p.next_appointment_at ?? "",
estado: p?.endereco?.estado ?? p?.estado ?? "", vip: Boolean(p.vip ?? false),
ultimoAtendimento: p.ultimo_atendimento ?? p.ultimoAtendimento ?? undefined, convenio: p.convenio ?? "", // se não existir, fica vazio
proximoAtendimento: p.proximo_atendimento ?? p.proximoAtendimento ?? undefined, status: p.status ?? undefined,
convenio: p.convenio ?? "", }));
vip: Boolean(p.vip ?? false),
status: p.status ?? undefined, setPatients((prev) => {
})); const all = [...prev, ...mapped];
setPatients((prev) => [...prev, ...mapped]); const unique = Array.from(new Map(all.map(p => [p.id, p])).values());
setHasNext(Boolean(json?.pagination?.has_next)); return unique;
setPage(pageToFetch + 1); });
if (mapped.length === 0) setHasNext(false); // parar carregamento
else setPage(prev => prev + 1);
} catch (e: any) { } catch (e: any) {
setError(e?.message || "Erro ao buscar pacientes"); setError(e?.message || "Erro ao buscar pacientes");
} finally { } finally {
@ -89,9 +91,21 @@ export default function PacientesPage() {
}; };
}, [fetchPacientes, page, hasNext, isFetching]); }, [fetchPacientes, page, hasNext, isFetching]);
const handleDeletePatient = (patientId: string) => { const handleDeletePatient = async (patientId: string) => {
// Remove from current list (client-side deletion) // Remove from current list (client-side deletion)
setPatients((prev) => prev.filter((p) => String(p.id) !== String(patientId))); try{
const res = await patientsService.delete(patientId);
if(res){
alert(`${res.error} ${res.message}`)
}
setPatients((prev) => prev.filter((p) => String(p.id) !== String(patientId)));
} catch (e: any) {
setError(e?.message || "Erro ao deletar paciente");
}
setDeleteDialogOpen(false); setDeleteDialogOpen(false);
setPatientToDelete(null); setPatientToDelete(null);
}; };
@ -311,33 +325,24 @@ export default function PacientesPage() {
<div className="text-red-600">{patientDetails.error}</div> <div className="text-red-600">{patientDetails.error}</div>
) : ( ) : (
<div className="space-y-2 text-left"> <div className="space-y-2 text-left">
<div> <p><strong>Nome:</strong> {patientDetails.full_name}</p>
<strong>Nome:</strong> {patientDetails.nome} <p><strong>CPF:</strong> {patientDetails.cpf}</p>
</div> <p><strong>Email:</strong> {patientDetails.email}</p>
<div> <p><strong>Telefone:</strong> {patientDetails.phone_mobile ?? patientDetails.phone1 ?? patientDetails.phone2 ?? "-"}</p>
<strong>Telefone:</strong> {patientDetails?.contato?.celular ?? patientDetails?.contato?.telefone1 ?? patientDetails?.telefone ?? ""} <p><strong>Nome social:</strong> {patientDetails.social_name ?? "-"}</p>
</div> <p><strong>Sexo:</strong> {patientDetails.sex ?? "-"}</p>
<div> <p><strong>Tipo sanguíneo:</strong> {patientDetails.blood_type ?? "-"}</p>
<strong>Cidade:</strong> {patientDetails?.endereco?.cidade ?? patientDetails?.cidade ?? ""} <p><strong>Peso:</strong> {patientDetails.weight_kg ?? "-"}{patientDetails.weight_kg ? "kg": ""}</p>
</div> <p><strong>Altura:</strong> {patientDetails.height_m ?? "-"}{patientDetails.height_m ? "m": ""}</p>
<div> <p><strong>IMC:</strong> {patientDetails.bmi ?? "-"}</p>
<strong>Estado:</strong> {patientDetails?.endereco?.estado ?? patientDetails?.estado ?? ""} <p><strong>Endereço:</strong> {patientDetails.street ?? "-"}</p>
</div> <p><strong>Bairro:</strong> {patientDetails.neighborhood ?? "-"}</p>
<div> <p><strong>Cidade:</strong> {patientDetails.city ?? "-"}</p>
<strong>Convênio:</strong> {patientDetails.convenio ?? ""} <p><strong>Estado:</strong> {patientDetails.state ?? "-"}</p>
</div> <p><strong>CEP:</strong> {patientDetails.cep ?? "-"}</p>
<div> <p><strong>Criado em:</strong> {patientDetails.created_at ?? "-"}</p>
<strong>VIP:</strong> {patientDetails.vip ? "Sim" : "Não"} <p><strong>Atualizado em:</strong> {patientDetails.updated_at ?? "-"}</p>
</div> <p><strong>Id:</strong> {patientDetails.id ?? "-"}</p>
<div>
<strong>Status:</strong> {patientDetails.status ?? ""}
</div>
<div>
<strong>Último atendimento:</strong> {patientDetails.ultimo_atendimento ?? patientDetails.ultimoAtendimento ?? ""}
</div>
<div>
<strong>Próximo atendimento:</strong> {patientDetails.proximo_atendimento ?? patientDetails.proximoAtendimento ?? ""}
</div>
</div> </div>
)} )}
</AlertDialogDescription> </AlertDialogDescription>

View File

@ -66,13 +66,13 @@ export default function SecretaryLayout({ children }: PatientLayoutProps) {
const menuItems = [ const menuItems = [
{ {
href: "#", href: "##",
icon: Home, icon: Home,
label: "Dashboard", label: "Dashboard",
// Botão para o dashboard da secretária // Botão para o dashboard da secretária
}, },
{ {
href: "#", href: "###",
icon: Calendar, icon: Calendar,
label: "Consultas", label: "Consultas",
// Botão para página de consultas marcadas // Botão para página de consultas marcadas

View File

@ -4,36 +4,38 @@ const BASE_URL = "https://yuanqfswhberkoevtmfr.supabase.co";
const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ"; const API_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl1YW5xZnN3aGJlcmtvZXZ0bWZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTQ5NTQzNjksImV4cCI6MjA3MDUzMDM2OX0.g8Fm4XAvtX46zifBZnYVH4tVuQkqUH6Ia9CXQj4DztQ";
var tempToken; var tempToken;
async function login() { export async function login() {
const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password", { const response = await fetch("https://yuanqfswhberkoevtmfr.supabase.co/auth/v1/token?grant_type=password", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
Prefer: "return=representation",
"apikey": API_KEY, // valor fixo "apikey": API_KEY, // valor fixo
}, },
body: JSON.stringify({ email: "hugo@popcode.com.br", password: "hdoria" }), body: JSON.stringify({ email: "riseup@popcode.com.br", password: "riseup" }),
}); });
const data = await response.json(); const data = await response.json();
console.log("Resposta da API:", data);
console.log("Token:", data.access_token);
// salvar o token do usuário // salvar o token do usuário
//localStorage.setItem("token", data.access_token); localStorage.setItem("token", data.access_token);
tempToken = data.access_token
return data; return data;
} }
await login()
async function run(){
await login()
}
run()
async function request(endpoint, options = {}) { async function request(endpoint, options = {}) {
//const token = localStorage.getItem("token"); // token do usuário, salvo no login const token = localStorage.getItem("token"); // token do usuário, salvo no login
const token = tempToken;
const headers = { const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"apikey": API_KEY, // obrigatório sempre "apikey": API_KEY, // obrigatório sempre
...(token ? { Authorization: `Bearer ${token}` } : {}), // obrigatório em todas EXCETO login ...(token ? { "Authorization": `Bearer ${token}` } : {}), // obrigatório em todas EXCETO login
...options.headers, ...options.headers,
}; };
@ -44,10 +46,19 @@ async function request(endpoint, options = {}) {
}); });
if (!response.ok) { if (!response.ok) {
const text = await response.text();
console.error("Erro HTTP:", response.status, text);
throw new Error(`Erro HTTP: ${response.status}`); throw new Error(`Erro HTTP: ${response.status}`);
} }
return await response.json(); // Lê a resposta como texto
const text = await response.text();
// Se não tiver conteúdo (204 ou 201 sem body), retorna null
if (!text) return null;
// Se tiver conteúdo, parseia como JSON
return JSON.parse(text);
} catch (error) { } catch (error) {
console.error("Erro na requisição:", error); console.error("Erro na requisição:", error);
throw error; throw error;

View File

@ -2,8 +2,8 @@ import { api } from "./api.mjs";
export const patientsService = { export const patientsService = {
list: () => api.get("/rest/v1/patients"), list: () => api.get("/rest/v1/patients"),
getById: (id) => api.get(`/rest/v1/patients/${id}`), getById: (id) => api.get(`/rest/v1/patients?id=eq.${id}`),
create: (data) => api.post("/rest/v1/patients", data), create: (data) => api.post("/rest/v1/patients", data),
update: (id, data) => api.patch(`/rest/v1/patients/${id}`, data), update: (id, data) => api.patch(`/rest/v1/patients?id=eq.${id}`, data),
delete: (id) => api.delete(`/rest/v1/patients/${id}`), delete: (id) => api.delete(`/rest/v1/patients?id=eq.${id}`),
}; };