1030 lines
43 KiB
TypeScript
1030 lines
43 KiB
TypeScript
|
|
import React, { useState, useEffect } from 'react'
|
|
import {Users, Calendar, FileText, Activity, Plus, Search, Filter, X, UserPlus} from 'lucide-react'
|
|
import toast from 'react-hot-toast'
|
|
import { useNavigate } from 'react-router-dom'
|
|
|
|
|
|
interface Paciente {
|
|
_id: string
|
|
nome: string
|
|
email: string
|
|
telefone: string
|
|
dataNascimento: string
|
|
altura?: number
|
|
peso?: number
|
|
endereco?: {
|
|
rua?: string
|
|
numero?: string
|
|
bairro?: string
|
|
cidade?: string
|
|
cep?: string
|
|
}
|
|
cpf?: string
|
|
convenio?: string
|
|
numeroCarteirinha?: string
|
|
observacoes?: string
|
|
}
|
|
|
|
interface Medico {
|
|
_id: string
|
|
nome: string
|
|
especialidade: string
|
|
crm: string
|
|
telefone: string
|
|
email: string
|
|
senha?: string
|
|
}
|
|
|
|
interface Consulta {
|
|
_id: string
|
|
paciente_id: string
|
|
medico_id: string
|
|
dataHora: string
|
|
tipo: string
|
|
status: string
|
|
observacoes?: string
|
|
pacienteNome?: string
|
|
medicoNome?: string
|
|
}
|
|
|
|
const PainelSecretaria: React.FC = () => {
|
|
const [activeTab, setActiveTab] = useState('dashboard')
|
|
const [pacientes, setPacientes] = useState<Paciente[]>([])
|
|
const [medicos, setMedicos] = useState<Medico[]>([])
|
|
const [consultas, setConsultas] = useState<Consulta[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [searchTerm, setSearchTerm] = useState('')
|
|
const [showNovoPacienteForm, setShowNovoPacienteForm] = useState(false)
|
|
const [showNovoMedicoForm, setShowNovoMedicoForm] = useState(false)
|
|
const navigate = useNavigate()
|
|
|
|
// Estado do formulário de novo paciente
|
|
const [formDataPaciente, setFormDataPaciente] = useState({
|
|
nome: '',
|
|
cpf: '',
|
|
telefone: '',
|
|
email: '',
|
|
dataNascimento: '',
|
|
altura: '',
|
|
peso: '',
|
|
endereco: {
|
|
rua: '',
|
|
numero: '',
|
|
bairro: '',
|
|
cidade: '',
|
|
cep: ''
|
|
},
|
|
convenio: '',
|
|
numeroCarteirinha: '',
|
|
observacoes: ''
|
|
})
|
|
|
|
// Estado do formulário de novo médico
|
|
const [formDataMedico, setFormDataMedico] = useState({
|
|
nome: '',
|
|
especialidade: '',
|
|
crm: '',
|
|
telefone: '',
|
|
email: '',
|
|
senha: ''
|
|
})
|
|
|
|
// Verificar se secretária está logada
|
|
useEffect(() => {
|
|
const secretariaLogada = localStorage.getItem('secretariaLogada')
|
|
if (!secretariaLogada) {
|
|
navigate('/secretaria')
|
|
return
|
|
}
|
|
carregarDados()
|
|
}, [navigate])
|
|
|
|
const carregarDados = async () => {
|
|
try {
|
|
setLoading(true)
|
|
// Carregar pacientes da API
|
|
const response = await fetch('https://mock.apidog.com/m1/1053378-0-default/pacientes')
|
|
const data = await response.json()
|
|
// A API retorna { success: true, data: [...] }
|
|
setPacientes(Array.isArray(data.data) ? data.data : [])
|
|
} catch (error) {
|
|
console.error('Erro ao carregar dados:', error)
|
|
toast.error('Erro ao carregar dados do sistema')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleLogout = () => {
|
|
localStorage.removeItem('secretariaLogada')
|
|
toast.success('Logout realizado com sucesso!')
|
|
navigate('/secretaria') // Navegar para login da secretária/gestão
|
|
}
|
|
|
|
const handleNovoPaciente = () => {
|
|
setShowNovoPacienteForm(true)
|
|
setActiveTab('pacientes') // Mudar para aba pacientes
|
|
}
|
|
|
|
const handleNovoMedico = () => {
|
|
setShowNovoMedicoForm(true)
|
|
setActiveTab('medicos') // Mudar para aba médicos
|
|
}
|
|
|
|
const resetFormPaciente = () => {
|
|
setFormDataPaciente({
|
|
nome: '',
|
|
cpf: '',
|
|
telefone: '',
|
|
email: '',
|
|
dataNascimento: '',
|
|
altura: '',
|
|
peso: '',
|
|
endereco: {
|
|
rua: '',
|
|
numero: '',
|
|
bairro: '',
|
|
cidade: '',
|
|
cep: ''
|
|
},
|
|
convenio: '',
|
|
numeroCarteirinha: '',
|
|
observacoes: ''
|
|
})
|
|
setShowNovoPacienteForm(false)
|
|
}
|
|
|
|
const resetFormMedico = () => {
|
|
setFormDataMedico({
|
|
nome: '',
|
|
especialidade: '',
|
|
crm: '',
|
|
telefone: '',
|
|
email: '',
|
|
senha: ''
|
|
})
|
|
setShowNovoMedicoForm(false)
|
|
}
|
|
|
|
const handleSubmitNovoPaciente = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
try {
|
|
setLoading(true)
|
|
|
|
// Montar o body conforme a API espera
|
|
const pacienteData = {
|
|
nome: formDataPaciente.nome,
|
|
cpf: formDataPaciente.cpf,
|
|
data_nascimento: formDataPaciente.dataNascimento,
|
|
telefone: formDataPaciente.telefone,
|
|
email: formDataPaciente.email,
|
|
// Adicione outros campos se a API aceitar
|
|
}
|
|
const response = await fetch('https://mock.apidog.com/m1/1053378-0-default/pacientes', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(pacienteData)
|
|
})
|
|
if (!response.ok) throw new Error('Erro ao cadastrar paciente na API')
|
|
const novoPaciente = await response.json()
|
|
setPacientes(prev => [...prev, novoPaciente])
|
|
resetFormPaciente()
|
|
toast.success('Paciente cadastrado com sucesso!')
|
|
// Opcional: carregarDados() // Se quiser garantir atualização da lista da API
|
|
} catch (error) {
|
|
console.error('Erro ao cadastrar paciente:', error)
|
|
toast.error('Erro ao cadastrar paciente. Tente novamente.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const handleSubmitNovoMedico = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
try {
|
|
setLoading(true)
|
|
|
|
const medicoData = {
|
|
...formDataMedico,
|
|
ativo: true,
|
|
criadoPor: 'secretaria',
|
|
criadoEm: new Date().toISOString()
|
|
}
|
|
|
|
// Aqui você pode fazer um POST para a API se ela aceitar, ou apenas comentar se não houver endpoint
|
|
toast.success('Médico cadastrado com sucesso!')
|
|
resetFormMedico()
|
|
carregarDados() // Recarregar dados
|
|
} catch (error) {
|
|
console.error('Erro ao cadastrar médico:', error)
|
|
toast.error('Erro ao cadastrar médico. Tente novamente.')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
const formatarData = (data: string) => {
|
|
return new Date(data).toLocaleDateString('pt-BR', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
}
|
|
|
|
const getStatusColor = (status: string) => {
|
|
switch (status) {
|
|
case 'agendada': return 'bg-blue-100 text-blue-800'
|
|
case 'confirmada': return 'bg-green-100 text-green-800'
|
|
case 'cancelada': return 'bg-red-100 text-red-800'
|
|
case 'realizada': return 'bg-gray-100 text-gray-800'
|
|
default: return 'bg-gray-100 text-gray-800'
|
|
}
|
|
}
|
|
|
|
// Filtrar dados baseado no termo de pesquisa
|
|
const pacientesFiltrados = pacientes.filter(p =>
|
|
(p.nome || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(p.email || '').toLowerCase().includes(searchTerm.toLowerCase())
|
|
)
|
|
|
|
const medicosFiltrados = medicos.filter(m =>
|
|
(m.nome || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(m.especialidade || '').toLowerCase().includes(searchTerm.toLowerCase())
|
|
)
|
|
|
|
const consultasFiltradas = consultas.filter(c =>
|
|
(c.pacienteNome || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(c.medicoNome || '').toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(c.tipo || '').toLowerCase().includes(searchTerm.toLowerCase())
|
|
)
|
|
|
|
if (loading && !showNovoPacienteForm && !showNovoMedicoForm) {
|
|
return (
|
|
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-green-600 mx-auto mb-4"></div>
|
|
<p className="text-gray-600">Carregando painel da secretária...</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gray-50">
|
|
{/* Header */}
|
|
<div className="bg-white shadow-sm border-b">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="flex justify-between items-center py-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Painel da Secretária</h1>
|
|
<p className="text-gray-600">Sistema de Gestão Médica</p>
|
|
</div>
|
|
<div className="flex space-x-3">
|
|
<button
|
|
onClick={handleNovoPaciente}
|
|
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors flex items-center"
|
|
>
|
|
<Plus className="w-5 h-5 mr-2" />
|
|
Novo Paciente
|
|
</button>
|
|
<button
|
|
onClick={handleNovoMedico}
|
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center"
|
|
>
|
|
<UserPlus className="w-5 h-5 mr-2" />
|
|
Novo Médico
|
|
</button>
|
|
<button
|
|
onClick={handleLogout}
|
|
className="bg-red-600 text-white px-4 py-2 rounded-lg hover:bg-red-700 transition-colors"
|
|
>
|
|
Sair
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Navigation Tabs */}
|
|
<div className="bg-white border-b">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<nav className="flex space-x-8">
|
|
{[
|
|
{ id: 'dashboard', label: 'Dashboard', icon: Activity },
|
|
{ id: 'pacientes', label: 'Pacientes', icon: Users },
|
|
{ id: 'medicos', label: 'Médicos', icon: Users },
|
|
{ id: 'consultas', label: 'Consultas', icon: Calendar },
|
|
{ id: 'relatorios', label: 'Relatórios', icon: FileText }
|
|
].map(tab => {
|
|
const Icon = tab.icon
|
|
return (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => setActiveTab(tab.id)}
|
|
className={`flex items-center px-3 py-4 border-b-2 font-medium text-sm ${
|
|
activeTab === tab.id
|
|
? 'border-green-500 text-green-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
<Icon className="w-5 h-5 mr-2" />
|
|
{tab.label}
|
|
</button>
|
|
)
|
|
})}
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
{/* Dashboard */}
|
|
{activeTab === 'dashboard' && (
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<div className="flex items-center">
|
|
<Users className="w-8 h-8 text-blue-600" />
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">Total Pacientes</p>
|
|
<p className="text-2xl font-bold text-gray-900">{pacientes.length}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<div className="flex items-center">
|
|
<Users className="w-8 h-8 text-green-600" />
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">Total Médicos</p>
|
|
<p className="text-2xl font-bold text-gray-900">{medicos.length}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<div className="flex items-center">
|
|
<Calendar className="w-8 h-8 text-purple-600" />
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">Consultas Hoje</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{consultas.filter(c => {
|
|
const hoje = new Date().toDateString()
|
|
const dataConsulta = new Date(c.dataHora).toDateString()
|
|
return dataConsulta === hoje
|
|
}).length}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<div className="flex items-center">
|
|
<Activity className="w-8 h-8 text-orange-600" />
|
|
<div className="ml-4">
|
|
<p className="text-sm font-medium text-gray-600">Consultas Pendentes</p>
|
|
<p className="text-2xl font-bold text-gray-900">
|
|
{consultas.filter(c => c.status === 'agendada').length}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Próximas Consultas */}
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="px-6 py-4 border-b">
|
|
<h3 className="text-lg font-medium text-gray-900">Próximas Consultas</h3>
|
|
</div>
|
|
<div className="p-6">
|
|
<div className="space-y-4">
|
|
{consultas
|
|
.filter(c => new Date(c.dataHora) >= new Date())
|
|
.sort((a, b) => new Date(a.dataHora).getTime() - new Date(b.dataHora).getTime())
|
|
.slice(0, 5)
|
|
.map(consulta => (
|
|
<div key={consulta._id} className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">
|
|
<div>
|
|
<p className="font-medium text-gray-900">{consulta.pacienteNome}</p>
|
|
<p className="text-sm text-gray-600">Dr(a). {consulta.medicoNome}</p>
|
|
<p className="text-sm text-gray-500">{formatarData(consulta.dataHora)}</p>
|
|
</div>
|
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(consulta.status)}`}>
|
|
{consulta.status}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Pacientes */}
|
|
{activeTab === 'pacientes' && (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-xl font-bold text-gray-900">Gerenciar Pacientes</h2>
|
|
<button
|
|
onClick={handleNovoPaciente}
|
|
className="bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors flex items-center"
|
|
>
|
|
<Plus className="w-5 h-5 mr-2" />
|
|
Novo Paciente
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="p-6 border-b">
|
|
<div className="flex items-center space-x-4">
|
|
<div className="relative flex-1">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar pacientes..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Paciente
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Contato
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Data Nascimento
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Altura/Peso
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{pacientesFiltrados.map(paciente => (
|
|
<tr key={paciente._id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-900">{paciente.nome || 'Nome não informado'}</div>
|
|
<div className="text-sm text-gray-500">{paciente.endereco?.cidade || 'Cidade não informada'}</div>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">{paciente.email || 'Email não informado'}</div>
|
|
<div className="text-sm text-gray-500">{paciente.telefone || 'Telefone não informado'}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{paciente.dataNascimento ? new Date(paciente.dataNascimento).toLocaleDateString('pt-BR') : 'Não informado'}
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
|
{paciente.altura && paciente.peso ? `${paciente.altura}m / ${paciente.peso}kg` : 'Não informado'}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Médicos */}
|
|
{activeTab === 'medicos' && (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-xl font-bold text-gray-900">Gerenciar Médicos</h2>
|
|
<button
|
|
onClick={handleNovoMedico}
|
|
className="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors flex items-center"
|
|
>
|
|
<UserPlus className="w-5 h-5 mr-2" />
|
|
Novo Médico
|
|
</button>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="p-6 border-b">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar médicos..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Médico
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Especialidade
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
CRM
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Contato
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{medicosFiltrados.map(medico => (
|
|
<tr key={medico._id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm font-medium text-gray-900">Dr(a). {medico.nome || 'Nome não informado'}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">{medico.especialidade || 'Não informado'}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">{medico.crm || 'Não informado'}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">{medico.email || 'Email não informado'}</div>
|
|
<div className="text-sm text-gray-500">{medico.telefone || 'Telefone não informado'}</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Consultas */}
|
|
{activeTab === 'consultas' && (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-xl font-bold text-gray-900">Gerenciar Consultas</h2>
|
|
</div>
|
|
|
|
<div className="bg-white rounded-lg shadow">
|
|
<div className="p-6 border-b">
|
|
<div className="relative">
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" />
|
|
<input
|
|
type="text"
|
|
placeholder="Buscar consultas..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="overflow-x-auto">
|
|
<table className="min-w-full divide-y divide-gray-200">
|
|
<thead className="bg-gray-50">
|
|
<tr>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Paciente
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Médico
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Data/Hora
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Tipo
|
|
</th>
|
|
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
Status
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="bg-white divide-y divide-gray-200">
|
|
{consultasFiltradas.map(consulta => (
|
|
<tr key={consulta._id} className="hover:bg-gray-50">
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm font-medium text-gray-900">{consulta.pacienteNome}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">Dr(a). {consulta.medicoNome}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">{formatarData(consulta.dataHora)}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<div className="text-sm text-gray-900">{consulta.tipo || 'Não informado'}</div>
|
|
</td>
|
|
<td className="px-6 py-4 whitespace-nowrap">
|
|
<span className={`px-3 py-1 rounded-full text-xs font-medium ${getStatusColor(consulta.status)}`}>
|
|
{consulta.status}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Relatórios */}
|
|
{activeTab === 'relatorios' && (
|
|
<div className="space-y-6">
|
|
<h2 className="text-xl font-bold text-gray-900">Relatórios</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Consultas por Status</h3>
|
|
<div className="space-y-3">
|
|
{['agendada', 'confirmada', 'realizada', 'cancelada'].map(status => {
|
|
const count = consultas.filter(c => c.status === status).length
|
|
const percentage = consultas.length > 0 ? (count / consultas.length) * 100 : 0
|
|
|
|
return (
|
|
<div key={status} className="flex items-center justify-between">
|
|
<span className="text-sm font-medium text-gray-600 capitalize">{status}</span>
|
|
<div className="flex items-center space-x-2">
|
|
<div className="w-20 bg-gray-200 rounded-full h-2">
|
|
<div
|
|
className="bg-green-600 h-2 rounded-full"
|
|
style={{ width: `${percentage}%` }}
|
|
></div>
|
|
</div>
|
|
<span className="text-sm font-medium text-gray-900">{count}</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white p-6 rounded-lg shadow">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Médicos por Especialidade</h3>
|
|
<div className="space-y-3">
|
|
{[...new Set(medicos.map(m => m.especialidade).filter(Boolean))].map(especialidade => {
|
|
const count = medicos.filter(m => m.especialidade === especialidade).length
|
|
|
|
return (
|
|
<div key={especialidade} className="flex items-center justify-between">
|
|
<span className="text-sm font-medium text-gray-600">{especialidade}</span>
|
|
<span className="text-sm font-medium text-gray-900">{count}</span>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Modal de Novo Paciente */}
|
|
{showNovoPacienteForm && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h3 className="text-lg font-semibold">Cadastrar Novo Paciente</h3>
|
|
<button
|
|
onClick={resetFormPaciente}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
<X className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmitNovoPaciente} className="space-y-4">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Nome Completo *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formDataPaciente.nome}
|
|
onChange={(e) => setFormDataPaciente({...formDataPaciente, nome: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
CPF *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formDataPaciente.cpf}
|
|
onChange={(e) => setFormDataPaciente({...formDataPaciente, cpf: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Telefone *
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={formDataPaciente.telefone}
|
|
onChange={(e) => setFormDataPaciente({...formDataPaciente, telefone: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Email *
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={formDataPaciente.email}
|
|
onChange={(e) => setFormDataPaciente({...formDataPaciente, email: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Data de Nascimento *
|
|
</label>
|
|
<input
|
|
type="date"
|
|
value={formDataPaciente.dataNascimento}
|
|
onChange={(e) => setFormDataPaciente({...formDataPaciente, dataNascimento: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Altura (cm)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="50"
|
|
max="250"
|
|
step="0.1"
|
|
value={formDataPaciente.altura}
|
|
onChange={(e) => setFormDataPaciente({...formDataPaciente, altura: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
placeholder="Ex: 170"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Peso (kg)
|
|
</label>
|
|
<input
|
|
type="number"
|
|
min="10"
|
|
max="300"
|
|
step="0.1"
|
|
value={formDataPaciente.peso}
|
|
onChange={(e) => setFormDataPaciente({...formDataPaciente, peso: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
placeholder="Ex: 70.5"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
CEP
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formDataPaciente.endereco.cep}
|
|
onChange={(e) => setFormDataPaciente({
|
|
...formDataPaciente,
|
|
endereco: {...formDataPaciente.endereco, cep: e.target.value}
|
|
})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Cidade
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formDataPaciente.endereco.cidade}
|
|
onChange={(e) => setFormDataPaciente({
|
|
...formDataPaciente,
|
|
endereco: {...formDataPaciente.endereco, cidade: e.target.value}
|
|
})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Convênio
|
|
</label>
|
|
<select
|
|
value={formDataPaciente.convenio}
|
|
onChange={(e) => setFormDataPaciente({...formDataPaciente, convenio: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
>
|
|
<option value="">Selecione</option>
|
|
<option value="Particular">Particular</option>
|
|
<option value="Unimed">Unimed</option>
|
|
<option value="SulAmérica">SulAmérica</option>
|
|
<option value="Bradesco Saúde">Bradesco Saúde</option>
|
|
<option value="Amil">Amil</option>
|
|
<option value="NotreDame">NotreDame</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Observações
|
|
</label>
|
|
<textarea
|
|
value={formDataPaciente.observacoes}
|
|
onChange={(e) => setFormDataPaciente({...formDataPaciente, observacoes: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-transparent"
|
|
rows={3}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-3 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={resetFormPaciente}
|
|
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50"
|
|
>
|
|
{loading ? 'Cadastrando...' : 'Cadastrar Paciente'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Modal de Novo Médico */}
|
|
{showNovoMedicoForm && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
|
<div className="bg-white rounded-lg max-w-md w-full max-h-[90vh] overflow-y-auto">
|
|
<div className="p-6">
|
|
<div className="flex justify-between items-center mb-6">
|
|
<h3 className="text-lg font-semibold">Cadastrar Novo Médico</h3>
|
|
<button
|
|
onClick={resetFormMedico}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
<X className="w-6 h-6" />
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmitNovoMedico} className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Nome Completo *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formDataMedico.nome}
|
|
onChange={(e) => setFormDataMedico({...formDataMedico, nome: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Especialidade *
|
|
</label>
|
|
<select
|
|
value={formDataMedico.especialidade}
|
|
onChange={(e) => setFormDataMedico({...formDataMedico, especialidade: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
required
|
|
>
|
|
<option value="">Selecione</option>
|
|
<option value="Cardiologia">Cardiologia</option>
|
|
<option value="Dermatologia">Dermatologia</option>
|
|
<option value="Endocrinologia">Endocrinologia</option>
|
|
<option value="Gastroenterologia">Gastroenterologia</option>
|
|
<option value="Ginecologia">Ginecologia</option>
|
|
<option value="Neurologia">Neurologia</option>
|
|
<option value="Oftalmologia">Oftalmologia</option>
|
|
<option value="Ortopedia">Ortopedia</option>
|
|
<option value="Pediatria">Pediatria</option>
|
|
<option value="Psiquiatria">Psiquiatria</option>
|
|
<option value="Urologia">Urologia</option>
|
|
<option value="Clínico Geral">Clínico Geral</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
CRM *
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={formDataMedico.crm}
|
|
onChange={(e) => setFormDataMedico({...formDataMedico, crm: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
placeholder="Ex: CRM/SP 123456"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Telefone *
|
|
</label>
|
|
<input
|
|
type="tel"
|
|
value={formDataMedico.telefone}
|
|
onChange={(e) => setFormDataMedico({...formDataMedico, telefone: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Email *
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={formDataMedico.email}
|
|
onChange={(e) => setFormDataMedico({...formDataMedico, email: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Senha *
|
|
</label>
|
|
<input
|
|
type="password"
|
|
value={formDataMedico.senha}
|
|
onChange={(e) => setFormDataMedico({...formDataMedico, senha: e.target.value})}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
required
|
|
minLength={6}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-3 pt-4">
|
|
<button
|
|
type="button"
|
|
onClick={resetFormMedico}
|
|
className="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-50 transition-colors"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={loading}
|
|
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
|
|
>
|
|
{loading ? 'Cadastrando...' : 'Cadastrar Médico'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default PainelSecretaria
|