feat: add list and registration of physicians

This commit is contained in:
M-Gabrielly 2025-09-11 03:40:34 -03:00
parent e06c4376cb
commit eceae602d4
7 changed files with 825 additions and 150 deletions

View File

@ -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<DoctorFormData[]>([])
const [loading, setLoading] = useState(true)
const [showDoctorForm, setShowDoctorForm] = useState(false)
const [editingDoctor, setEditingDoctor] = useState<DoctorFormData | null>(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 (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" onClick={handleFormClose} className="p-2">
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-bold text-foreground">
{editingDoctor ? "Editar Médico" : "Cadastrar Novo Médico"}
</h1>
<p className="text-muted-foreground">
{editingDoctor ? "Atualize as informações do médico" : "Preencha os dados do novo médico"}
</p>
</div>
</div>
<DoctorRegistrationForm doctorData={editingDoctor} onClose={handleFormClose} />
</div>
)
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-foreground">Médicos</h1>
<p className="text-muted-foreground">Gerencie os médicos da clínica</p>
</div>
<Button className="bg-primary hover:bg-primary/90" onClick={handleAddDoctor}>
<Plus className="mr-2 h-4 w-4" />
Adicionar Médico
</Button>
</div>
<div className="flex flex-wrap gap-4 items-center">
<div className="relative flex-1 min-w-64">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
<Input
placeholder="Buscar por nome, CRM ou especialidade"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>Nome</TableHead>
<TableHead>CRM</TableHead>
<TableHead>Especialidade</TableHead>
<TableHead>Celular</TableHead>
<TableHead>E-mail</TableHead>
<TableHead className="w-[100px]">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={6} className="text-center">Carregando...</TableCell>
</TableRow>
) : filteredMedicos.map((medico) => (
<TableRow key={medico.id}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-muted rounded-full flex items-center justify-center">
<span className="text-xs font-medium">{medico.nome.charAt(0).toUpperCase()}</span>
</div>
<button onClick={() => handleViewDetails(medico.id!)} className="hover:text-primary cursor-pointer">
{medico.nome}
</button>
</div>
</TableCell>
<TableCell>{medico.crm} - {medico.crmUf}</TableCell>
<TableCell>{medico.especialidade}</TableCell>
<TableCell>{medico.celular}</TableCell>
<TableCell>{medico.email}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="h-8 w-8 p-0 flex items-center justify-center rounded-md hover:bg-accent">
<span className="sr-only">Abrir menu</span>
<MoreHorizontal className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleViewDetails(medico.id!)}>
<Eye className="mr-2 h-4 w-4" />
Ver detalhes
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEditDoctor(medico)}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDeleteDoctor(medico.id!)} className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="text-sm text-muted-foreground">
Mostrando {filteredMedicos.length} de {medicos.length} médicos
</div>
</div>
)
}

View File

@ -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 },

View File

@ -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<DoctorFormData>(initialFormData)
const [expandedSections, setExpandedSections] = useState({
dadosPessoais: true,
dadosProfissionais: true,
contato: true,
endereco: true,
observacoes: false,
})
const [photoPreview, setPhotoPreview] = useState<string | null>(null)
const [isLoadingCep, setIsLoadingCep] = useState(false)
const [errors, setErrors] = useState<Record<string, string>>({})
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<HTMLInputElement>) => {
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<string, string> = {}
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 (
<form onSubmit={handleSubmit} className="space-y-6">
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">Dados do Médico</h2>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="nome">Nome</Label>
<Input id="nome" value={formData.nome} onChange={e => handleInputChange("nome", e.target.value)} />
{errors.nome && <p className="text-sm text-destructive">{errors.nome}</p>}
</div>
<div>
<Label htmlFor="crm">CRM</Label>
<Input id="crm" value={formData.crm} onChange={e => handleInputChange("crm", e.target.value)} />
{errors.crm && <p className="text-sm text-destructive">{errors.crm}</p>}
</div>
<div>
<Label htmlFor="especialidade">Especialidade</Label>
<Input id="especialidade" value={formData.especialidade} onChange={e => handleInputChange("especialidade", e.target.value)} />
{errors.especialidade && <p className="text-sm text-destructive">{errors.especialidade}</p>}
</div>
<div>
<Label htmlFor="email">E-mail</Label>
<Input id="email" value={formData.email} onChange={e => handleInputChange("email", e.target.value)} />
{errors.email && <p className="text-sm text-destructive">{errors.email}</p>}
</div>
<div>
<Label htmlFor="celular">Celular</Label>
<Input id="celular" value={formData.celular} onChange={e => handleInputChange("celular", e.target.value)} />
{errors.celular && <p className="text-sm text-destructive">{errors.celular}</p>}
</div>
<div>
<Label htmlFor="cpf">CPF</Label>
<Input id="cpf" value={formData.cpf} onChange={e => handleInputChange("cpf", e.target.value)} />
{errors.cpf && <p className="text-sm text-destructive">{errors.cpf}</p>}
</div>
<div>
<Label htmlFor="dataNascimento">Data de Nascimento</Label>
<Input id="dataNascimento" type="date" value={formData.dataNascimento} onChange={e => handleInputChange("dataNascimento", e.target.value)} />
{errors.dataNascimento && <p className="text-sm text-destructive">{errors.dataNascimento}</p>}
</div>
</CardContent>
</Card>
{errors.submit && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{errors.submit}</AlertDescription>
</Alert>
)}
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">Endereço</h2>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="cep">CEP</Label>
<Input id="cep" value={formData.cep} onChange={e => handleInputChange("cep", e.target.value)} />
{errors.cep && <p className="text-sm text-destructive">{errors.cep}</p>}
</div>
<div>
<Label htmlFor="logradouro">Logradouro</Label>
<Input id="logradouro" value={formData.logradouro} onChange={e => handleInputChange("logradouro", e.target.value)} />
{errors.logradouro && <p className="text-sm text-destructive">{errors.logradouro}</p>}
</div>
<div>
<Label htmlFor="numero">Número</Label>
<Input id="numero" value={formData.numero} onChange={e => handleInputChange("numero", e.target.value)} />
{errors.numero && <p className="text-sm text-destructive">{errors.numero}</p>}
</div>
<div>
<Label htmlFor="bairro">Bairro</Label>
<Input id="bairro" value={formData.bairro} onChange={e => handleInputChange("bairro", e.target.value)} />
{errors.bairro && <p className="text-sm text-destructive">{errors.bairro}</p>}
</div>
<div>
<Label htmlFor="cidade">Cidade</Label>
<Input id="cidade" value={formData.cidade} onChange={e => handleInputChange("cidade", e.target.value)} />
{errors.cidade && <p className="text-sm text-destructive">{errors.cidade}</p>}
</div>
<div>
<Label htmlFor="estado">Estado</Label>
<Input id="estado" value={formData.estado} onChange={e => handleInputChange("estado", e.target.value)} />
{errors.estado && <p className="text-sm text-destructive">{errors.estado}</p>}
</div>
</CardContent>
</Card>
{/* Dados Pessoais */}
<Collapsible open={expandedSections.dadosPessoais} onOpenChange={() => toggleSection("dadosPessoais")}>
<Card>
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer">
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2"><User className="h-4 w-4" />Dados Pessoais</span>
{expandedSections.dadosPessoais ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</CardTitle>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="space-y-4 pt-4">
<div className="flex items-center gap-4">
<div className="w-24 h-24 border-2 border-dashed rounded-lg flex items-center justify-center overflow-hidden">
{photoPreview ? (
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
) : (
<FileImage className="h-8 w-8 text-muted-foreground" />
)}
</div>
<div className="space-y-2">
<Label htmlFor="photo" className="cursor-pointer">
<Button type="button" variant="outline" asChild>
<label htmlFor="photo" className="cursor-pointer"><Upload className="mr-2 h-4 w-4" />Carregar Foto</label>
</Button>
</Label>
<Input id="photo" type="file" accept="image/*" className="hidden" onChange={handlePhotoUpload} />
{errors.photo && <p className="text-sm text-destructive">{errors.photo}</p>}
<p className="text-xs text-muted-foreground">Máximo 5MB</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="nome">Nome *</Label>
<Input id="nome" value={formData.nome} onChange={(e) => handleInputChange("nome", e.target.value)} className={errors.nome ? "border-destructive" : ""} />
{errors.nome && <p className="text-sm text-destructive">{errors.nome}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="nomeSocial">Nome Social</Label>
<Input id="nomeSocial" value={formData.nomeSocial || ''} onChange={(e) => handleInputChange("nomeSocial", e.target.value)} />
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cpf">CPF *</Label>
<Input id="cpf" value={formData.cpf} onChange={(e) => handleInputChange("cpf", e.target.value)} placeholder="000.000.000-00" className={errors.cpf ? "border-destructive" : ""} />
{errors.cpf && <p className="text-sm text-destructive">{errors.cpf}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="rg">RG</Label>
<Input id="rg" value={formData.rg} onChange={(e) => handleInputChange("rg", e.target.value)} placeholder="00.000.000-0"/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Sexo</Label>
<Select value={formData.sexo} onValueChange={(value) => handleInputChange("sexo", value)}>
<SelectTrigger><SelectValue placeholder="Selecione o sexo" /></SelectTrigger>
<SelectContent>
<SelectItem value="masculino">Masculino</SelectItem>
<SelectItem value="feminino">Feminino</SelectItem>
<SelectItem value="outro">Outro</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="dataNascimento">Data de Nascimento</Label>
<Input id="dataNascimento" type="date" value={formData.dataNascimento} onChange={(e) => handleInputChange("dataNascimento", e.target.value)} />
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
<Card>
<CardHeader>
<h2 className="text-lg font-semibold">Observações</h2>
</CardHeader>
<CardContent>
<Label htmlFor="observacoes">Observações</Label>
<Input id="observacoes" value={formData.observacoes} onChange={e => handleInputChange("observacoes", e.target.value)} />
</CardContent>
</Card>
{/* Dados Profissionais */}
<Collapsible open={expandedSections.dadosProfissionais} onOpenChange={() => toggleSection("dadosProfissionais")}>
<Card>
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer">
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2"><Stethoscope className="h-4 w-4" />Dados Profissionais</span>
{expandedSections.dadosProfissionais ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</CardTitle>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="space-y-4 pt-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="crm">CRM *</Label>
<Input id="crm" value={formData.crm} onChange={(e) => handleInputChange("crm", e.target.value)} className={errors.crm ? "border-destructive" : ""} />
{errors.crm && <p className="text-sm text-destructive">{errors.crm}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="crmUf">UF do CRM *</Label>
<Input id="crmUf" value={formData.crmUf} onChange={(e) => handleInputChange("crmUf", e.target.value)} className={errors.crmUf ? "border-destructive" : ""} />
{errors.crmUf && <p className="text-sm text-destructive">{errors.crmUf}</p>}
</div>
</div>
<div className="space-y-2">
<Label htmlFor="especialidade">Especialidade *</Label>
<Popover open={openEspecialidade} onOpenChange={setOpenEspecialidade}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={openEspecialidade}
className={`w-full justify-between ${errors.especialidade ? "border-destructive" : ""}`}>
{formData.especialidade || "Selecione a especialidade"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command>
<CommandInput placeholder="Buscar especialidade..." />
<CommandEmpty>Nenhuma especialidade encontrada.</CommandEmpty>
<ScrollArea className="h-72">
<CommandGroup>
{especialidadesMedicas.map((especialidade) => (
<CommandItem
key={especialidade}
value={especialidade}
onSelect={(currentValue) => {
handleInputChange("especialidade", currentValue === formData.especialidade ? "" : currentValue)
setOpenEspecialidade(false)
}}>
<Check
className={`mr-2 h-4 w-4 ${formData.especialidade === especialidade ? "opacity-100" : "opacity-0"}`}
/>
{especialidade}
</CommandItem>
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
{errors.especialidade && <p className="text-sm text-destructive">{errors.especialidade}</p>}
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
<div className="flex justify-end gap-2">
<Button type="button" variant="outline">Cancelar</Button>
<Button type="submit">Cadastrar Médico</Button>
{/* Contato */}
<Collapsible open={expandedSections.contato} onOpenChange={() => toggleSection("contato")}>
<Card>
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer">
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2"><Phone className="h-4 w-4" />Contato</span>
{expandedSections.contato ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</CardTitle>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="space-y-4 pt-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="email">E-mail *</Label>
<Input id="email" type="email" value={formData.email} onChange={(e) => handleInputChange("email", e.target.value)} className={errors.email ? "border-destructive" : ""} />
{errors.email && <p className="text-sm text-destructive">{errors.email}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="celular">Celular *</Label>
<Input id="celular" value={formData.celular} onChange={(e) => handleInputChange("celular", e.target.value)} placeholder="(XX) XXXXX-XXXX" className={errors.celular ? "border-destructive" : ""} />
{errors.celular && <p className="text-sm text-destructive">{errors.celular}</p>}
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
{/* Endereço */}
<Collapsible open={expandedSections.endereco} onOpenChange={() => toggleSection("endereco")}>
<Card>
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer">
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2"><MapPin className="h-4 w-4" />Endereço</span>
{expandedSections.endereco ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</CardTitle>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="space-y-4 pt-4">
<div className="grid grid-cols-3 gap-4 items-end">
<div className="space-y-2 col-span-1">
<Label htmlFor="cep">CEP *</Label>
<Input id="cep" value={formData.cep} onChange={(e) => handleInputChange("cep", e.target.value)} onBlur={(e) => searchCEP(e.target.value)} disabled={isLoadingCep} className={errors.cep ? "border-destructive" : ""} />
{errors.cep && <p className="text-sm text-destructive">{errors.cep}</p>}
</div>
{isLoadingCep && <Loader2 className="h-4 w-4 animate-spin" />}
</div>
<div className="space-y-2">
<Label htmlFor="logradouro">Logradouro *</Label>
<Input id="logradouro" value={formData.logradouro} onChange={(e) => handleInputChange("logradouro", e.target.value)} className={errors.logradouro ? "border-destructive" : ""} />
{errors.logradouro && <p className="text-sm text-destructive">{errors.logradouro}</p>}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="numero">Número *</Label>
<Input id="numero" value={formData.numero} onChange={(e) => handleInputChange("numero", e.target.value)} className={errors.numero ? "border-destructive" : ""} />
{errors.numero && <p className="text-sm text-destructive">{errors.numero}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="complemento">Complemento</Label>
<Input id="complemento" value={formData.complemento || ''} onChange={(e) => handleInputChange("complemento", e.target.value)} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="bairro">Bairro *</Label>
<Input id="bairro" value={formData.bairro} onChange={(e) => handleInputChange("bairro", e.target.value)} className={errors.bairro ? "border-destructive" : ""} />
{errors.bairro && <p className="text-sm text-destructive">{errors.bairro}</p>}
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="cidade">Cidade *</Label>
<Input id="cidade" value={formData.cidade} onChange={(e) => handleInputChange("cidade", e.target.value)} className={errors.cidade ? "border-destructive" : ""} />
{errors.cidade && <p className="text-sm text-destructive">{errors.cidade}</p>}
</div>
<div className="space-y-2">
<Label htmlFor="estado">Estado *</Label>
<Input id="estado" value={formData.estado} onChange={(e) => handleInputChange("estado", e.target.value)} className={errors.estado ? "border-destructive" : ""} />
{errors.estado && <p className="text-sm text-destructive">{errors.estado}</p>}
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
{/* Observações */}
<Collapsible open={expandedSections.observacoes} onOpenChange={() => toggleSection("observacoes")}>
<Card>
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer">
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2"><Stethoscope className="h-4 w-4" />Observações</span>
{expandedSections.observacoes ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
</CardTitle>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="pt-4">
<Textarea
id="observacoes"
value={formData.observacoes || ''}
onChange={(e) => handleInputChange("observacoes", e.target.value)}
placeholder="Notas adicionais sobre o médico..."
rows={4}
/>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
<div className="flex justify-end gap-4 pt-6 border-t">
<Button type="button" variant="outline" onClick={onClose} disabled={isSubmitting}>
<XCircle className="mr-2 h-4 w-4" />
Cancelar
</Button>
<Button type="submit" className="bg-primary hover:bg-primary/90" disabled={isSubmitting}>
{isSubmitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Save className="mr-2 h-4 w-4" />}
{isSubmitting ? "Salvando..." : doctorData?.id ? "Atualizar Médico" : "Salvar Médico"}
</Button>
</div>
</form>
)
}
}

View File

@ -35,25 +35,19 @@ const buttonVariants = cva(
}
)
function Button({
className,
variant,
size,
asChild = false,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Button = React.forwardRef<
HTMLButtonElement,
React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants> & { asChild?: boolean }
>(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
data-slot="button"
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
})
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -1,23 +0,0 @@
export async function salvarPaciente(formData) {
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/json");
var raw = JSON.stringify(formData);
var requestOptions = {
method: 'POST',
headers: myHeaders,
body: raw,
redirect: 'follow'
};
try {
const response = await fetch("https://mock.apidog.com/m1/1053378-0-default/pacientes", requestOptions);
const result = await response.json();
return result;
} catch (error) {
console.log('error', error);
throw error;
}
}

View File

@ -11,3 +11,104 @@ export async function salvarPaciente(formData: Record<string, any>) {
throw error
}
}
// Define the Doctor data interface
export interface DoctorFormData {
id?: number;
nome: string;
nomeSocial?: string;
cpf: string;
rg: string;
crm: string;
crmUf: string;
especialidade: string;
email: string;
celular: string;
dataNascimento: string;
sexo: string;
cep: string;
logradouro: string;
numero: string;
complemento?: string;
bairro: string;
cidade: string;
estado: string;
observacoes?: string;
photo?: File | null;
}
// Mock data for doctors
const medicosMock: DoctorFormData[] = [
{
id: 1,
nome: "Dr. João da Silva",
nomeSocial: "",
cpf: "111.111.111-11",
rg: "11.111.111-1",
crm: "12345",
crmUf: "SP",
especialidade: "Cardiologia",
email: "joao.silva@example.com",
celular: "(11) 99999-1234",
dataNascimento: "1980-01-15",
sexo: "masculino",
cep: "01001-000",
logradouro: "Praça da Sé",
numero: "1",
bairro: "Sé",
cidade: "São Paulo",
estado: "SP",
},
{
id: 2,
nome: "Dra. Maria Oliveira",
nomeSocial: "Dra. Maria",
cpf: "222.222.222-22",
rg: "22.222.222-2",
crm: "54321",
crmUf: "RJ",
especialidade: "Dermatologia",
email: "maria.oliveira@example.com",
celular: "(21) 98888-5678",
dataNascimento: "1985-05-20",
sexo: "feminino",
cep: "20031-050",
logradouro: "Av. Pres. Wilson",
numero: "231",
bairro: "Centro",
cidade: "Rio de Janeiro",
estado: "RJ",
},
];
export async function getMedicos(): Promise<DoctorFormData[]> {
return new Promise(resolve => {
setTimeout(() => {
resolve(medicosMock);
}, 500);
});
}
export async function getMedicoById(id: number): Promise<DoctorFormData | undefined> {
return new Promise(resolve => {
setTimeout(() => {
const medico = medicosMock.find(m => m.id === id);
resolve(medico);
}, 300);
});
}
export async function salvarMedico(formData: DoctorFormData) {
try {
const response = await axios.post("https://mock.apidog.com/m1/1053378-0-default/medicos", formData,
{ headers: { "Content-Type": "application/json" } }
);
console.log("Médico salvo com sucesso:", response.data);
return response.data;
} catch (error) {
console.error('Erro ao salvar médico:', error);
throw error;
}
}

View File

@ -0,0 +1,26 @@
export const formatCPF = (cpf: string): string => {
const cleaned = cpf.replace(/\D/g, "");
const match = cleaned.match(/^(\d{3})(\d{3})(\d{3})(\d{2})$/);
if (match) {
return `${match[1]}.${match[2]}.${match[3]}-${match[4]}`;
}
return cpf;
};
export const formatCelular = (celular: string): string => {
const cleaned = celular.replace(/\D/g, "");
const match = cleaned.match(/^(\d{2})(\d{5})(\d{4})$/);
if (match) {
return `(${match[1]}) ${match[2]}-${match[3]}`;
}
return celular;
};
export const formatRG = (rg: string): string => {
const cleaned = rg.replace(/\D/g, "");
const match = cleaned.match(/^(\d{2})(\d{3})(\d{3})(\d{1})$/);
if (match) {
return `${match[1]}.${match[2]}.${match[3]}-${match[4]}`;
}
return rg;
};