diff --git a/susconecta/app/dashboard/medicos/page.tsx b/susconecta/app/dashboard/medicos/page.tsx new file mode 100644 index 0000000..9bc215a --- /dev/null +++ b/susconecta/app/dashboard/medicos/page.tsx @@ -0,0 +1,188 @@ +'use client' + +import { useState, useEffect } from "react" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table" +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu" +import { + Search, + Plus, + MoreHorizontal, + Eye, + Edit, + Trash2, + ArrowLeft, +} from "lucide-react" +import { DoctorRegistrationForm } from "@/components/forms/doctor-registration-form" +import { getMedicos, DoctorFormData } from "@/lib/api" + +export default function MedicosPage() { + const [searchTerm, setSearchTerm] = useState("") + const [medicos, setMedicos] = useState([]) + const [loading, setLoading] = useState(true) + const [showDoctorForm, setShowDoctorForm] = useState(false) + const [editingDoctor, setEditingDoctor] = useState(null) + + async function fetchMedicos() { + try { + setLoading(true) + const data = await getMedicos() + setMedicos(data) + } catch (error) { + console.error("Erro ao buscar médicos:", error) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchMedicos() + }, []) + + const filteredMedicos = medicos.filter((medico) => + medico.nome.toLowerCase().includes(searchTerm.toLowerCase()) || + (medico.crm && medico.crm.toLowerCase().includes(searchTerm.toLowerCase())) || + (medico.especialidade && medico.especialidade.toLowerCase().includes(searchTerm.toLowerCase())) + ) + + const handleViewDetails = (doctorId: number) => { + console.log("[v0] Ver detalhes do médico:", doctorId) + } + + const handleEditDoctor = (doctor: DoctorFormData) => { + setEditingDoctor(doctor) + setShowDoctorForm(true) + } + + const handleDeleteDoctor = (doctorId: number) => { + setMedicos(medicos.filter(m => m.id !== doctorId)) + } + + const handleAddDoctor = () => { + setEditingDoctor(null) + setShowDoctorForm(true) + } + + const handleFormClose = () => { + setShowDoctorForm(false) + setEditingDoctor(null) + fetchMedicos() // Recarrega os dados após fechar o formulário + } + + if (showDoctorForm) { + return ( +
+
+ +
+

+ {editingDoctor ? "Editar Médico" : "Cadastrar Novo Médico"} +

+

+ {editingDoctor ? "Atualize as informações do médico" : "Preencha os dados do novo médico"} +

+
+
+ + +
+ ) + } + + return ( +
+
+
+

Médicos

+

Gerencie os médicos da clínica

+
+ +
+ +
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+
+ +
+ + + + Nome + CRM + Especialidade + Celular + E-mail + Ações + + + + {loading ? ( + + Carregando... + + ) : filteredMedicos.map((medico) => ( + + +
+
+ {medico.nome.charAt(0).toUpperCase()} +
+ +
+
+ {medico.crm} - {medico.crmUf} + {medico.especialidade} + {medico.celular} + {medico.email} + + + + + + + handleViewDetails(medico.id!)}> + + Ver detalhes + + handleEditDoctor(medico)}> + + Editar + + handleDeleteDoctor(medico.id!)} className="text-destructive"> + + Excluir + + + + +
+ ))} +
+
+
+ +
+ Mostrando {filteredMedicos.length} de {medicos.length} médicos +
+
+ ) +} diff --git a/susconecta/components/dashboard/sidebar.tsx b/susconecta/components/dashboard/sidebar.tsx index f277ea9..736426a 100644 --- a/susconecta/components/dashboard/sidebar.tsx +++ b/susconecta/components/dashboard/sidebar.tsx @@ -9,6 +9,7 @@ const navigation = [ { name: "Dashboard", href: "/dashboard", icon: Home }, { name: "Agenda", href: "/dashboard/agenda", icon: Calendar }, { name: "Pacientes", href: "/dashboard/pacientes", icon: Users }, + { name: "Médicos", href: "/dashboard/medicos", icon: Stethoscope }, { name: "Consultas", href: "/dashboard/consultas", icon: UserCheck }, { name: "Prontuários", href: "/dashboard/prontuarios", icon: FileText }, { name: "Relatórios", href: "/dashboard/relatorios", icon: BarChart3 }, diff --git a/susconecta/components/forms/doctor-registration-form.tsx b/susconecta/components/forms/doctor-registration-form.tsx index f875ea2..a4b5e93 100644 --- a/susconecta/components/forms/doctor-registration-form.tsx +++ b/susconecta/components/forms/doctor-registration-form.tsx @@ -1,157 +1,545 @@ -"use client" +'use client' -import { useState } from "react" -import { Input } from "@/components/ui/input" +import type React from "react" +import { useState, useEffect } from "react" import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" -import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible" +import { Alert, AlertDescription } from "@/components/ui/alert" +import { salvarMedico, DoctorFormData } from "@/lib/api"; +import { formatCPF, formatCelular, formatRG } from "@/lib/formatters"; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from "@/components/ui/command" +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" +import { ScrollArea } from "@/components/ui/scroll-area" +import { + Upload, + ChevronDown, + ChevronUp, + X, + FileImage, + User, + Phone, + MapPin, + Save, + XCircle, + AlertCircle, + Loader2, + Stethoscope, + Check, + ChevronsUpDown +} from "lucide-react" -export function DoctorRegistrationForm() { - const [formData, setFormData] = useState({ +interface DoctorRegistrationFormProps { + doctorData?: DoctorFormData | null + onClose?: () => void +} + +const especialidadesMedicas = [ + "Acupuntura", + "Alergia e Imunologia", + "Anestesiologia", + "Angiologia", + "Cancerologia (Oncologia)", + "Cardiologia", + "Cirurgia Cardiovascular", + "Cirurgia da Mão", + "Cirurgia de Cabeça e Pescoço", + "Cirurgia do Aparelho Digestivo", + "Cirurgia Geral", + "Cirurgia Oncológica", + "Cirurgia Pediátrica", + "Cirurgia Plástica", + "Cirurgia Torácica", + "Cirurgia Vascular", + "Clínica Médica", + "Coloproctologia", + "Dermatologia", + "Endocrinologia e Metabologia", + "Endoscopia", + "Gastroenterologia", + "Genética Médica", + "Geriatria", + "Ginecologia e Obstetrícia", + "Hematologia e Hemoterapia", + "Homeopatia", + "Infectologia", + "Mastologia", + "Medicina de Emergência", + "Medicina de Família e Comunidade", + "Medicina do Trabalho", + "Medicina de Tráfego", + "Medicina Esportiva", + "Medicina Física e Reabilitação", + "Medicina Intensiva", + "Medicina Legal e Perícia Médica", + "Medicina Nuclear", + "Medicina Preventiva e Social", + "Nefrologia", + "Neurocirurgia", + "Neurologia", + "Nutrologia", + "Oftalmologia", + "Ortopedia e Traumatologia", + "Otorrinolaringologia", + "Patologia", + "Patologia Clínica/Medicina Laboratorial", + "Pediatria", + "Pneumologia", + "Psiquiatria", + "Radiologia e Diagnóstico por Imagem", + "Radioterapia", + "Reumatologia", + "Urologia", +]; + +export function DoctorRegistrationForm({ + doctorData = null, + onClose, +}: DoctorRegistrationFormProps) { + + const initialFormData: DoctorFormData = { nome: "", + nomeSocial: "", + cpf: "", + rg: "", crm: "", + crmUf: "", especialidade: "", email: "", celular: "", - cpf: "", dataNascimento: "", + sexo: "", cep: "", - logradouro: "", + logradouro: "", numero: "", + complemento: "", bairro: "", cidade: "", estado: "", observacoes: "", - }) - const [errors, setErrors] = useState({}) + photo: null, + } - const handleInputChange = (field, value) => { - setFormData((prev) => ({ ...prev, [field]: value })) + const [formData, setFormData] = useState(initialFormData) + const [expandedSections, setExpandedSections] = useState({ + dadosPessoais: true, + dadosProfissionais: true, + contato: true, + endereco: true, + observacoes: false, + }) + + const [photoPreview, setPhotoPreview] = useState(null) + const [isLoadingCep, setIsLoadingCep] = useState(false) + const [errors, setErrors] = useState>({}) + const [isSubmitting, setIsSubmitting] = useState(false) + const [openEspecialidade, setOpenEspecialidade] = useState(false) + + useEffect(() => { + if (doctorData) { + setFormData(doctorData); + if (doctorData.photo && doctorData.photo instanceof File) { + const reader = new FileReader() + reader.onload = (e) => setPhotoPreview(e.target?.result as string) + reader.readAsDataURL(doctorData.photo) + } + } else { + setFormData(initialFormData); + setPhotoPreview(null); + } + }, [doctorData]) + + const toggleSection = (section: keyof typeof expandedSections) => { + setExpandedSections((prev) => ({ + ...prev, + [section]: !prev[section], + })) + } + + const handleInputChange = (field: keyof DoctorFormData, value: string | boolean | File | File[] | null) => { + let formattedValue = value; + if (typeof value === 'string') { + if (field === "cpf") { + formattedValue = formatCPF(value); + } else if (field === "celular") { + formattedValue = formatCelular(value); + } else if (field === "rg") { + formattedValue = formatRG(value); + } + } + + setFormData((prev) => ({ + ...prev, + [field]: formattedValue, + })) + + if (errors[field]) { + setErrors((prev) => { + const newErrors = { ...prev } + delete newErrors[field] + return newErrors + }) + } + } + + const handlePhotoUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0] + if (file) { + if (file.size > 5 * 1024 * 1024) { // 5MB limit + setErrors((prev) => ({ ...prev, photo: "Arquivo muito grande. Máximo 5MB." })) + return + } + handleInputChange("photo", file) + const reader = new FileReader() + reader.onload = (e) => setPhotoPreview(e.target?.result as string) + reader.readAsDataURL(file) + } + } + + const searchCEP = async (cep: string) => { + const cleanCEP = cep.replace(/\D/g, "") + if (cleanCEP.length !== 8) return + + setIsLoadingCep(true) + try { + const response = await fetch(`https://viacep.com.br/ws/${cleanCEP}/json/`) + const data = await response.json() + + if (data.erro) { + setErrors((prev) => ({ ...prev, cep: "CEP não encontrado" })) + } else { + handleInputChange("logradouro", data.logradouro || "") + handleInputChange("bairro", data.bairro || "") + handleInputChange("cidade", data.localidade || "") + handleInputChange("estado", data.uf || "") + } + } catch (error) { + console.error("Erro ao buscar CEP:", error) + setErrors((prev) => ({ ...prev, cep: "Erro ao buscar CEP. Tente novamente." })) + } finally { + setIsLoadingCep(false) + } } const validateForm = () => { - const newErrors = {} + const newErrors: Record = {} if (!formData.nome.trim()) newErrors.nome = "Nome é obrigatório" - if (!formData.crm.trim()) newErrors.crm = "CRM é obrigatório" - if (!formData.especialidade.trim()) newErrors.especialidade = "Especialidade é obrigatória" - if (!formData.email.trim()) newErrors.email = "E-mail é obrigatório" - if (!formData.celular.trim()) newErrors.celular = "Celular é obrigatório" if (!formData.cpf.trim()) newErrors.cpf = "CPF é obrigatório" - if (!formData.dataNascimento.trim()) newErrors.dataNascimento = "Data de nascimento é obrigatória" + if (!formData.crm.trim()) newErrors.crm = "CRM é obrigatório" + if (!formData.crmUf.trim()) newErrors.crmUf = "UF do CRM é obrigatória" + if (!formData.especialidade.trim()) newErrors.especialidade = "Especialidade é obrigatória" + if (!formData.celular.trim()) newErrors.celular = "Celular é obrigatório" + if (!formData.email.trim()) newErrors.email = "E-mail é obrigatório" if (!formData.cep.trim()) newErrors.cep = "CEP é obrigatório" if (!formData.logradouro.trim()) newErrors.logradouro = "Logradouro é obrigatório" if (!formData.numero.trim()) newErrors.numero = "Número é obrigatório" if (!formData.bairro.trim()) newErrors.bairro = "Bairro é obrigatório" if (!formData.cidade.trim()) newErrors.cidade = "Cidade é obrigatória" if (!formData.estado.trim()) newErrors.estado = "Estado é obrigatório" + setErrors(newErrors) return Object.keys(newErrors).length === 0 } - const handleSubmit = (e) => { - e.preventDefault() + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault() if (!validateForm()) return - // Aqui você pode chamar a API para salvar o médico - alert("Médico cadastrado com sucesso!") + + setIsSubmitting(true) + try { + await salvarMedico(formData) + alert(doctorData?.id ? "Médico atualizado com sucesso!" : "Médico cadastrado com sucesso!") + if (onClose) onClose() + } catch (error) { + setErrors({ submit: "Erro ao salvar médico. Tente novamente." }) + } finally { + setIsSubmitting(false) + } } return (
- - -

Dados do Médico

-
- -
- - handleInputChange("nome", e.target.value)} /> - {errors.nome &&

{errors.nome}

} -
-
- - handleInputChange("crm", e.target.value)} /> - {errors.crm &&

{errors.crm}

} -
-
- - handleInputChange("especialidade", e.target.value)} /> - {errors.especialidade &&

{errors.especialidade}

} -
-
- - handleInputChange("email", e.target.value)} /> - {errors.email &&

{errors.email}

} -
-
- - handleInputChange("celular", e.target.value)} /> - {errors.celular &&

{errors.celular}

} -
-
- - handleInputChange("cpf", e.target.value)} /> - {errors.cpf &&

{errors.cpf}

} -
-
- - handleInputChange("dataNascimento", e.target.value)} /> - {errors.dataNascimento &&

{errors.dataNascimento}

} -
-
-
+ {errors.submit && ( + + + {errors.submit} + + )} - - -

Endereço

-
- -
- - handleInputChange("cep", e.target.value)} /> - {errors.cep &&

{errors.cep}

} -
-
- - handleInputChange("logradouro", e.target.value)} /> - {errors.logradouro &&

{errors.logradouro}

} -
-
- - handleInputChange("numero", e.target.value)} /> - {errors.numero &&

{errors.numero}

} -
-
- - handleInputChange("bairro", e.target.value)} /> - {errors.bairro &&

{errors.bairro}

} -
-
- - handleInputChange("cidade", e.target.value)} /> - {errors.cidade &&

{errors.cidade}

} -
-
- - handleInputChange("estado", e.target.value)} /> - {errors.estado &&

{errors.estado}

} -
-
-
+ {/* Dados Pessoais */} + toggleSection("dadosPessoais")}> + + + + + Dados Pessoais + {expandedSections.dadosPessoais ? : } + + + + + +
+
+ {photoPreview ? ( + Preview + ) : ( + + )} +
+
+ + + {errors.photo &&

{errors.photo}

} +

Máximo 5MB

+
+
+
+
+ + handleInputChange("nome", e.target.value)} className={errors.nome ? "border-destructive" : ""} /> + {errors.nome &&

{errors.nome}

} +
+
+ + handleInputChange("nomeSocial", e.target.value)} /> +
+
+
+
+ + handleInputChange("cpf", e.target.value)} placeholder="000.000.000-00" className={errors.cpf ? "border-destructive" : ""} /> + {errors.cpf &&

{errors.cpf}

} +
+
+ + handleInputChange("rg", e.target.value)} placeholder="00.000.000-0"/> +
+
+
+
+ + +
+
+ + handleInputChange("dataNascimento", e.target.value)} /> +
+
+
+
+
+
- - -

Observações

-
- - - handleInputChange("observacoes", e.target.value)} /> - -
+ {/* Dados Profissionais */} + toggleSection("dadosProfissionais")}> + + + + + Dados Profissionais + {expandedSections.dadosProfissionais ? : } + + + + + +
+
+ + handleInputChange("crm", e.target.value)} className={errors.crm ? "border-destructive" : ""} /> + {errors.crm &&

{errors.crm}

} +
+
+ + handleInputChange("crmUf", e.target.value)} className={errors.crmUf ? "border-destructive" : ""} /> + {errors.crmUf &&

{errors.crmUf}

} +
+
+
+ + + + + + + + + Nenhuma especialidade encontrada. + + + {especialidadesMedicas.map((especialidade) => ( + { + handleInputChange("especialidade", currentValue === formData.especialidade ? "" : currentValue) + setOpenEspecialidade(false) + }}> + + {especialidade} + + ))} + + + + + + {errors.especialidade &&

{errors.especialidade}

} +
+
+
+
+
-
- - + {/* Contato */} + toggleSection("contato")}> + + + + + Contato + {expandedSections.contato ? : } + + + + + +
+
+ + handleInputChange("email", e.target.value)} className={errors.email ? "border-destructive" : ""} /> + {errors.email &&

{errors.email}

} +
+
+ + handleInputChange("celular", e.target.value)} placeholder="(XX) XXXXX-XXXX" className={errors.celular ? "border-destructive" : ""} /> + {errors.celular &&

{errors.celular}

} +
+
+
+
+
+
+ + {/* Endereço */} + toggleSection("endereco")}> + + + + + Endereço + {expandedSections.endereco ? : } + + + + + +
+
+ + handleInputChange("cep", e.target.value)} onBlur={(e) => searchCEP(e.target.value)} disabled={isLoadingCep} className={errors.cep ? "border-destructive" : ""} /> + {errors.cep &&

{errors.cep}

} +
+ {isLoadingCep && } +
+
+ + handleInputChange("logradouro", e.target.value)} className={errors.logradouro ? "border-destructive" : ""} /> + {errors.logradouro &&

{errors.logradouro}

} +
+
+
+ + handleInputChange("numero", e.target.value)} className={errors.numero ? "border-destructive" : ""} /> + {errors.numero &&

{errors.numero}

} +
+
+ + handleInputChange("complemento", e.target.value)} /> +
+
+
+ + handleInputChange("bairro", e.target.value)} className={errors.bairro ? "border-destructive" : ""} /> + {errors.bairro &&

{errors.bairro}

} +
+
+
+ + handleInputChange("cidade", e.target.value)} className={errors.cidade ? "border-destructive" : ""} /> + {errors.cidade &&

{errors.cidade}

} +
+
+ + handleInputChange("estado", e.target.value)} className={errors.estado ? "border-destructive" : ""} /> + {errors.estado &&

{errors.estado}

} +
+
+
+
+
+
+ + {/* Observações */} + toggleSection("observacoes")}> + + + + + Observações + {expandedSections.observacoes ? : } + + + + + +