feat: add list and registration of physicians
This commit is contained in:
parent
e06c4376cb
commit
eceae602d4
188
susconecta/app/dashboard/medicos/page.tsx
Normal file
188
susconecta/app/dashboard/medicos/page.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ const navigation = [
|
|||||||
{ name: "Dashboard", href: "/dashboard", icon: Home },
|
{ name: "Dashboard", href: "/dashboard", icon: Home },
|
||||||
{ name: "Agenda", href: "/dashboard/agenda", icon: Calendar },
|
{ name: "Agenda", href: "/dashboard/agenda", icon: Calendar },
|
||||||
{ name: "Pacientes", href: "/dashboard/pacientes", icon: Users },
|
{ name: "Pacientes", href: "/dashboard/pacientes", icon: Users },
|
||||||
|
{ name: "Médicos", href: "/dashboard/medicos", icon: Stethoscope },
|
||||||
{ name: "Consultas", href: "/dashboard/consultas", icon: UserCheck },
|
{ name: "Consultas", href: "/dashboard/consultas", icon: UserCheck },
|
||||||
{ name: "Prontuários", href: "/dashboard/prontuarios", icon: FileText },
|
{ name: "Prontuários", href: "/dashboard/prontuarios", icon: FileText },
|
||||||
{ name: "Relatórios", href: "/dashboard/relatorios", icon: BarChart3 },
|
{ name: "Relatórios", href: "/dashboard/relatorios", icon: BarChart3 },
|
||||||
|
|||||||
@ -1,157 +1,545 @@
|
|||||||
"use client"
|
'use client'
|
||||||
|
|
||||||
import { useState } from "react"
|
import type React from "react"
|
||||||
import { Input } from "@/components/ui/input"
|
import { useState, useEffect } from "react"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
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() {
|
interface DoctorRegistrationFormProps {
|
||||||
const [formData, setFormData] = useState({
|
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: "",
|
nome: "",
|
||||||
|
nomeSocial: "",
|
||||||
|
cpf: "",
|
||||||
|
rg: "",
|
||||||
crm: "",
|
crm: "",
|
||||||
|
crmUf: "",
|
||||||
especialidade: "",
|
especialidade: "",
|
||||||
email: "",
|
email: "",
|
||||||
celular: "",
|
celular: "",
|
||||||
cpf: "",
|
|
||||||
dataNascimento: "",
|
dataNascimento: "",
|
||||||
|
sexo: "",
|
||||||
cep: "",
|
cep: "",
|
||||||
logradouro: "",
|
logradouro: "",
|
||||||
numero: "",
|
numero: "",
|
||||||
|
complemento: "",
|
||||||
bairro: "",
|
bairro: "",
|
||||||
cidade: "",
|
cidade: "",
|
||||||
estado: "",
|
estado: "",
|
||||||
observacoes: "",
|
observacoes: "",
|
||||||
})
|
photo: null,
|
||||||
const [errors, setErrors] = useState({})
|
}
|
||||||
|
|
||||||
const handleInputChange = (field, value) => {
|
const [formData, setFormData] = useState<DoctorFormData>(initialFormData)
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }))
|
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 validateForm = () => {
|
||||||
const newErrors = {}
|
const newErrors: Record<string, string> = {}
|
||||||
if (!formData.nome.trim()) newErrors.nome = "Nome é obrigatório"
|
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.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.cep.trim()) newErrors.cep = "CEP é obrigatório"
|
||||||
if (!formData.logradouro.trim()) newErrors.logradouro = "Logradouro é obrigatório"
|
if (!formData.logradouro.trim()) newErrors.logradouro = "Logradouro é obrigatório"
|
||||||
if (!formData.numero.trim()) newErrors.numero = "Número é obrigatório"
|
if (!formData.numero.trim()) newErrors.numero = "Número é obrigatório"
|
||||||
if (!formData.bairro.trim()) newErrors.bairro = "Bairro é obrigatório"
|
if (!formData.bairro.trim()) newErrors.bairro = "Bairro é obrigatório"
|
||||||
if (!formData.cidade.trim()) newErrors.cidade = "Cidade é obrigatória"
|
if (!formData.cidade.trim()) newErrors.cidade = "Cidade é obrigatória"
|
||||||
if (!formData.estado.trim()) newErrors.estado = "Estado é obrigatório"
|
if (!formData.estado.trim()) newErrors.estado = "Estado é obrigatório"
|
||||||
|
|
||||||
setErrors(newErrors)
|
setErrors(newErrors)
|
||||||
return Object.keys(newErrors).length === 0
|
return Object.keys(newErrors).length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = async (event: React.FormEvent) => {
|
||||||
e.preventDefault()
|
event.preventDefault()
|
||||||
if (!validateForm()) return
|
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 (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<Card>
|
{errors.submit && (
|
||||||
<CardHeader>
|
<Alert variant="destructive">
|
||||||
<h2 className="text-lg font-semibold">Dados do Médico</h2>
|
<AlertCircle className="h-4 w-4" />
|
||||||
</CardHeader>
|
<AlertDescription>{errors.submit}</AlertDescription>
|
||||||
<CardContent className="space-y-4">
|
</Alert>
|
||||||
<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>
|
|
||||||
|
|
||||||
<Card>
|
{/* Dados Pessoais */}
|
||||||
<CardHeader>
|
<Collapsible open={expandedSections.dadosPessoais} onOpenChange={() => toggleSection("dadosPessoais")}>
|
||||||
<h2 className="text-lg font-semibold">Endereço</h2>
|
<Card>
|
||||||
</CardHeader>
|
<CollapsibleTrigger asChild>
|
||||||
<CardContent className="space-y-4">
|
<CardHeader className="cursor-pointer">
|
||||||
<div>
|
<CardTitle className="flex items-center justify-between">
|
||||||
<Label htmlFor="cep">CEP</Label>
|
<span className="flex items-center gap-2"><User className="h-4 w-4" />Dados Pessoais</span>
|
||||||
<Input id="cep" value={formData.cep} onChange={e => handleInputChange("cep", e.target.value)} />
|
{expandedSections.dadosPessoais ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
{errors.cep && <p className="text-sm text-destructive">{errors.cep}</p>}
|
</CardTitle>
|
||||||
</div>
|
</CardHeader>
|
||||||
<div>
|
</CollapsibleTrigger>
|
||||||
<Label htmlFor="logradouro">Logradouro</Label>
|
<CollapsibleContent>
|
||||||
<Input id="logradouro" value={formData.logradouro} onChange={e => handleInputChange("logradouro", e.target.value)} />
|
<CardContent className="space-y-4 pt-4">
|
||||||
{errors.logradouro && <p className="text-sm text-destructive">{errors.logradouro}</p>}
|
<div className="flex items-center gap-4">
|
||||||
</div>
|
<div className="w-24 h-24 border-2 border-dashed rounded-lg flex items-center justify-center overflow-hidden">
|
||||||
<div>
|
{photoPreview ? (
|
||||||
<Label htmlFor="numero">Número</Label>
|
<img src={photoPreview} alt="Preview" className="w-full h-full object-cover" />
|
||||||
<Input id="numero" value={formData.numero} onChange={e => handleInputChange("numero", e.target.value)} />
|
) : (
|
||||||
{errors.numero && <p className="text-sm text-destructive">{errors.numero}</p>}
|
<FileImage className="h-8 w-8 text-muted-foreground" />
|
||||||
</div>
|
)}
|
||||||
<div>
|
</div>
|
||||||
<Label htmlFor="bairro">Bairro</Label>
|
<div className="space-y-2">
|
||||||
<Input id="bairro" value={formData.bairro} onChange={e => handleInputChange("bairro", e.target.value)} />
|
<Label htmlFor="photo" className="cursor-pointer">
|
||||||
{errors.bairro && <p className="text-sm text-destructive">{errors.bairro}</p>}
|
<Button type="button" variant="outline" asChild>
|
||||||
</div>
|
<label htmlFor="photo" className="cursor-pointer"><Upload className="mr-2 h-4 w-4" />Carregar Foto</label>
|
||||||
<div>
|
</Button>
|
||||||
<Label htmlFor="cidade">Cidade</Label>
|
</Label>
|
||||||
<Input id="cidade" value={formData.cidade} onChange={e => handleInputChange("cidade", e.target.value)} />
|
<Input id="photo" type="file" accept="image/*" className="hidden" onChange={handlePhotoUpload} />
|
||||||
{errors.cidade && <p className="text-sm text-destructive">{errors.cidade}</p>}
|
{errors.photo && <p className="text-sm text-destructive">{errors.photo}</p>}
|
||||||
</div>
|
<p className="text-xs text-muted-foreground">Máximo 5MB</p>
|
||||||
<div>
|
</div>
|
||||||
<Label htmlFor="estado">Estado</Label>
|
</div>
|
||||||
<Input id="estado" value={formData.estado} onChange={e => handleInputChange("estado", e.target.value)} />
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{errors.estado && <p className="text-sm text-destructive">{errors.estado}</p>}
|
<div className="space-y-2">
|
||||||
</div>
|
<Label htmlFor="nome">Nome *</Label>
|
||||||
</CardContent>
|
<Input id="nome" value={formData.nome} onChange={(e) => handleInputChange("nome", e.target.value)} className={errors.nome ? "border-destructive" : ""} />
|
||||||
</Card>
|
{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>
|
{/* Dados Profissionais */}
|
||||||
<CardHeader>
|
<Collapsible open={expandedSections.dadosProfissionais} onOpenChange={() => toggleSection("dadosProfissionais")}>
|
||||||
<h2 className="text-lg font-semibold">Observações</h2>
|
<Card>
|
||||||
</CardHeader>
|
<CollapsibleTrigger asChild>
|
||||||
<CardContent>
|
<CardHeader className="cursor-pointer">
|
||||||
<Label htmlFor="observacoes">Observações</Label>
|
<CardTitle className="flex items-center justify-between">
|
||||||
<Input id="observacoes" value={formData.observacoes} onChange={e => handleInputChange("observacoes", e.target.value)} />
|
<span className="flex items-center gap-2"><Stethoscope className="h-4 w-4" />Dados Profissionais</span>
|
||||||
</CardContent>
|
{expandedSections.dadosProfissionais ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
</Card>
|
</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">
|
{/* Contato */}
|
||||||
<Button type="button" variant="outline">Cancelar</Button>
|
<Collapsible open={expandedSections.contato} onOpenChange={() => toggleSection("contato")}>
|
||||||
<Button type="submit">Cadastrar Médico</Button>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,25 +35,19 @@ const buttonVariants = cva(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
function Button({
|
const Button = React.forwardRef<
|
||||||
className,
|
HTMLButtonElement,
|
||||||
variant,
|
React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants> & { asChild?: boolean }
|
||||||
size,
|
>(({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
asChild = false,
|
|
||||||
...props
|
|
||||||
}: React.ComponentProps<"button"> &
|
|
||||||
VariantProps<typeof buttonVariants> & {
|
|
||||||
asChild?: boolean
|
|
||||||
}) {
|
|
||||||
const Comp = asChild ? Slot : "button"
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Comp
|
<Comp
|
||||||
data-slot="button"
|
|
||||||
className={cn(buttonVariants({ variant, size, className }))}
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
})
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
export { Button, buttonVariants }
|
export { Button, buttonVariants }
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -11,3 +11,104 @@ export async function salvarPaciente(formData: Record<string, any>) {
|
|||||||
throw error
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
26
susconecta/lib/formatters.ts
Normal file
26
susconecta/lib/formatters.ts
Normal 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;
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user