develop #83

Merged
M-Gabrielly merged 426 commits from develop into main 2025-12-04 04:13:15 +00:00
9 changed files with 672 additions and 562 deletions
Showing only changes of commit 9adf479e90 - Show all commits

View File

@ -469,76 +469,78 @@ export default function ConsultasPage() {
}
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center justify-between gap-4 flex-wrap">
<div className="space-y-4 sm:space-y-6 p-3 sm:p-4 md:p-6 bg-background">
{/* Header responsivo */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl font-bold">Gerenciamento de Consultas</h1>
<p className="text-muted-foreground">Visualize, filtre e gerencie todas as consultas da clínica.</p>
<h1 className="text-2xl sm:text-3xl font-bold">Consultas</h1>
<p className="text-xs sm:text-sm text-muted-foreground">Gerencie todas as consultas da clínica</p>
</div>
<div className="flex items-center gap-2">
{/* Pass origin so the Agenda page can return to Consultas when cancelling */}
<Link href="/agenda?origin=consultas">
<Button size="sm" className="h-8 gap-1 bg-blue-600">
<PlusCircle className="h-3.5 w-3.5" />
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">Agendar Nova Consulta</span>
</Button>
</Link>
<Link href="/agenda?origin=consultas">
<Button className="w-full sm:w-auto h-8 sm:h-9 gap-1 bg-blue-600 text-xs sm:text-sm">
<PlusCircle className="h-3.5 w-3.5" />
<span className="hidden sm:inline">Agendar</span>
<span className="sm:hidden">Nova</span>
</Button>
</Link>
</div>
{/* Filtros e busca responsivos */}
<div className="space-y-2 sm:space-y-3 p-3 sm:p-4 border rounded-lg bg-card">
{/* Linha 1: Busca */}
<div className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar…"
className="pl-8 w-full text-xs sm:text-sm h-8 sm:h-9 shadow-sm border"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onKeyDown={handleSearchKeyDown}
/>
</div>
</div>
{/* Linha 2: Selects responsivos */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
<Select onValueChange={(v) => { setSelectedStatus(String(v)); }}>
<SelectTrigger className="h-8 sm:h-9 text-xs sm:text-sm">
<SelectValue placeholder="Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="confirmed">Confirmada</SelectItem>
<SelectItem value="requested">Pendente</SelectItem>
<SelectItem value="cancelled">Cancelada</SelectItem>
</SelectContent>
</Select>
<Input type="date" className="h-8 sm:h-9 text-xs sm:text-sm" value={filterDate} onChange={(e) => setFilterDate(e.target.value)} />
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Consultas Agendadas</CardTitle>
<CardDescription>Visualize, filtre e gerencie todas as consultas da clínica.</CardDescription>
<div className="pt-4 flex flex-wrap items-center gap-4">
<div className="flex-1 min-w-[250px] flex gap-2 items-center">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
type="search"
placeholder="Buscar por..."
className="pl-8 pr-4 w-full shadow-sm border border-border bg-transparent"
value={searchValue}
onChange={(e) => setSearchValue(e.target.value)}
onKeyDown={handleSearchKeyDown}
/>
</div>
</div>
<Select onValueChange={(v) => { setSelectedStatus(String(v)); }}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filtrar por status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos</SelectItem>
<SelectItem value="confirmed">Confirmada</SelectItem>
{/* backend uses 'requested' for pending requests, map UI label to that value */}
<SelectItem value="requested">Pendente</SelectItem>
<SelectItem value="cancelled">Cancelada</SelectItem>
</SelectContent>
</Select>
<Input type="date" className="w-[180px]" value={filterDate} onChange={(e) => setFilterDate(e.target.value)} />
</div>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="w-full py-12 flex justify-center items-center">
<Loader2 className="animate-spin mr-2" />
<span>Carregando agendamentos...</span>
</div>
) : (
{/* Loading state */}
{isLoading ? (
<div className="w-full py-12 flex justify-center items-center border rounded-lg">
<Loader2 className="animate-spin mr-2" />
<span className="text-xs sm:text-sm">Carregando agendamentos...</span>
</div>
) : (
<>
{/* Desktop Table - Hidden on mobile */}
<div className="hidden md:block border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-primary hover:bg-primary">
<TableHead className="text-primary-foreground">Paciente</TableHead>
<TableHead className="text-primary-foreground">Médico</TableHead>
<TableHead className="text-primary-foreground">Status</TableHead>
<TableHead className="text-primary-foreground">Data e Hora</TableHead>
<TableHead className="text-primary-foreground">Ações</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Paciente</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Médico</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Status</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Data e Hora</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedAppointments.map((appointment) => {
// appointment.professional may now contain the doctor's name (resolved)
const professionalLookup = mockProfessionals.find((p) => p.id === appointment.professional);
const professionalName = typeof appointment.professional === "string" && appointment.professional && !professionalLookup
? appointment.professional
@ -546,8 +548,8 @@ export default function ConsultasPage() {
return (
<TableRow key={appointment.id}>
<TableCell className="font-medium">{appointment.patient}</TableCell>
<TableCell>{professionalName}</TableCell>
<TableCell className="font-medium text-xs sm:text-sm">{appointment.patient}</TableCell>
<TableCell className="text-xs sm:text-sm">{professionalName}</TableCell>
<TableCell>
<Badge
variant={
@ -562,12 +564,12 @@ export default function ConsultasPage() {
{capitalize(appointment.status)}
</Badge>
</TableCell>
<TableCell>{formatDate(appointment.scheduled_at ?? appointment.time)}</TableCell>
<TableCell className="text-xs sm:text-sm">{formatDate(appointment.scheduled_at ?? appointment.time)}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="h-8 w-8 p-0 flex items-center justify-center rounded-md hover:bg-primary hover:text-white transition-colors">
<span className="sr-only">Abrir menu</span>
<span className="sr-only">Menu</span>
<MoreHorizontal className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
@ -592,68 +594,146 @@ export default function ConsultasPage() {
})}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
{/* Controles de paginação */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Itens por página:</span>
{/* Mobile Cards - Hidden on desktop */}
<div className="md:hidden space-y-2">
{paginatedAppointments.length > 0 ? (
paginatedAppointments.map((appointment) => {
const professionalLookup = mockProfessionals.find((p) => p.id === appointment.professional);
const professionalName = typeof appointment.professional === "string" && appointment.professional && !professionalLookup
? appointment.professional
: (professionalLookup ? professionalLookup.name : (appointment.professional || "Não encontrado"));
return (
<div key={appointment.id} className="bg-card p-3 sm:p-4 rounded-lg border border-border hover:border-primary transition-colors">
<div className="grid grid-cols-2 gap-2">
<div className="col-span-2 flex justify-between items-start">
<div className="flex-1">
<div className="text-[10px] sm:text-xs font-semibold text-primary">Paciente</div>
<div className="text-xs sm:text-sm font-medium truncate">{appointment.patient}</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="h-7 w-7 p-0 flex items-center justify-center rounded-md hover:bg-primary hover:text-white transition-colors flex-shrink-0">
<span className="sr-only">Menu</span>
<MoreHorizontal className="h-3.5 w-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleView(appointment)}>
<Eye className="mr-2 h-4 w-4" />
Ver
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(appointment)}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(appointment.id)} className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Médico</div>
<div className="text-[10px] sm:text-xs font-medium truncate">{professionalName}</div>
</div>
<div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Status</div>
<Badge
variant={
appointment.status === "confirmed"
? "default"
: appointment.status === "pending"
? "secondary"
: "destructive"
}
className={`text-[10px] sm:text-xs ${appointment.status === "confirmed" ? "bg-green-600" : ""}`}
>
{capitalize(appointment.status)}
</Badge>
</div>
<div className="col-span-2">
<div className="text-[10px] sm:text-xs text-muted-foreground">Data e Hora</div>
<div className="text-[10px] sm:text-xs font-medium">{formatDate(appointment.scheduled_at ?? appointment.time)}</div>
</div>
</div>
</div>
);
})
) : (
<div className="text-center text-xs sm:text-sm text-muted-foreground py-4">
Nenhuma consulta encontrada
</div>
)}
</div>
</>
)}
{/* Controles de paginação - Responsivos */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4 text-xs sm:text-sm">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-muted-foreground text-xs sm:text-sm">Itens por página:</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
className="h-8 sm:h-9 rounded-md border border-input bg-background px-2 sm:px-3 py-1 text-xs sm:text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
<span className="text-sm text-muted-foreground">
<span className="text-muted-foreground text-xs sm:text-sm">
Mostrando {paginatedAppointments.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
{Math.min(currentPage * itemsPerPage, appointments.length)} de {appointments.length}
</span>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 sm:gap-2 flex-wrap justify-center sm:justify-end">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Primeira
<span className="hidden sm:inline">Primeira</span>
<span className="sm:hidden">1ª</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Anterior
<span className="hidden sm:inline">Anterior</span>
<span className="sm:hidden">«</span>
</Button>
<span className="text-sm text-muted-foreground">
Página {currentPage} de {totalPages || 1}
<span className="text-muted-foreground text-xs sm:text-sm">
Pág {currentPage} de {totalPages || 1}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Próxima
<span className="hidden sm:inline">Próxima</span>
<span className="sm:hidden">»</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Última
<span className="hidden sm:inline">Última</span>
<span className="sm:hidden">Últ</span>
</Button>
</div>
</div>

View File

@ -155,12 +155,12 @@ export default function DashboardPage() {
if (loading) {
return (
<div className="space-y-6 p-6 bg-background">
<div className="space-y-4 sm:space-y-6 p-3 sm:p-4 md:p-6 bg-background">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-muted rounded w-1/4"></div>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<div className="h-6 sm:h-8 bg-muted rounded w-1/4"></div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 md:gap-6">
{[1, 2, 3, 4].map(i => (
<div key={i} className="h-32 bg-muted rounded"></div>
<div key={i} className="h-24 sm:h-32 bg-muted rounded"></div>
))}
</div>
</div>
@ -171,15 +171,15 @@ export default function DashboardPage() {
// Se está exibindo formulário de paciente
if (showPatientForm) {
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center gap-4">
<Button variant="ghost" onClick={() => {
<div className="space-y-4 sm:space-y-6 p-3 sm:p-4 md:p-6 bg-background min-h-screen">
<div className="flex items-center gap-2 sm:gap-4">
<Button variant="ghost" size="icon" onClick={() => {
setShowPatientForm(false);
setEditingPatientId(null);
}}>
<ArrowLeft className="h-4 w-4" />
}} className="h-8 w-8 sm:h-10 sm:w-10">
<ArrowLeft className="h-4 w-4 sm:h-5 sm:w-5" />
</Button>
<h1 className="text-2xl font-bold">{editingPatientId ? "Editar paciente" : "Novo paciente"}</h1>
<h1 className="text-xl sm:text-2xl font-bold">{editingPatientId ? "Editar paciente" : "Novo paciente"}</h1>
</div>
<PatientRegistrationForm
@ -199,15 +199,15 @@ export default function DashboardPage() {
// Se está exibindo formulário de médico
if (showDoctorForm) {
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center gap-4">
<div className="space-y-4 sm:space-y-6 p-3 sm:p-4 md:p-6 bg-background min-h-screen">
<div className="flex items-center gap-2 sm:gap-4">
<Button variant="ghost" size="icon" onClick={() => {
setShowDoctorForm(false);
setEditingDoctorId(null);
}}>
<ArrowLeft className="h-4 w-4" />
}} className="h-8 w-8 sm:h-10 sm:w-10">
<ArrowLeft className="h-4 w-4 sm:h-5 sm:w-5" />
</Button>
<h1 className="text-2xl font-bold">{editingDoctorId ? "Editar Médico" : "Novo Médico"}</h1>
<h1 className="text-xl sm:text-2xl font-bold">{editingDoctorId ? "Editar Médico" : "Novo Médico"}</h1>
</div>
<DoctorRegistrationForm
@ -225,95 +225,99 @@ export default function DashboardPage() {
}
return (
<div className="space-y-6 p-6 bg-background">
{/* Header */}
<div className="space-y-4 sm:space-y-6 p-3 sm:p-4 md:p-6 bg-background min-h-screen">
{/* Header - Responsivo */}
<div>
<h1 className="text-3xl font-bold text-foreground">Dashboard</h1>
<p className="text-muted-foreground mt-2">Bem-vindo ao painel de controle</p>
<h1 className="text-2xl sm:text-3xl font-bold text-foreground">Dashboard</h1>
<p className="text-xs sm:text-sm text-muted-foreground mt-1 sm:mt-2">Bem-vindo ao painel de controle</p>
</div>
{/* 1. CARDS RESUMO */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-card p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Total de Pacientes</h3>
<p className="text-3xl font-bold text-foreground mt-2">{stats.totalPatients}</p>
{/* 1. CARDS RESUMO - Responsivo com 1/2/4 colunas */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4 md:gap-6">
<div className="bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<h3 className="text-xs sm:text-sm font-medium text-muted-foreground truncate">Total de Pacientes</h3>
<p className="text-2xl sm:text-3xl font-bold text-foreground mt-1 sm:mt-2">{stats.totalPatients}</p>
</div>
<Users className="h-8 w-8 text-blue-500 opacity-20" />
<Users className="h-6 sm:h-8 w-6 sm:w-8 text-blue-500 opacity-20 flex-shrink-0" />
</div>
</div>
<div className="bg-card p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Total de Médicos</h3>
<p className="text-3xl font-bold text-foreground mt-2">{stats.totalDoctors}</p>
<div className="bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<h3 className="text-xs sm:text-sm font-medium text-muted-foreground truncate">Total de Médicos</h3>
<p className="text-2xl sm:text-3xl font-bold text-foreground mt-1 sm:mt-2">{stats.totalDoctors}</p>
</div>
<Stethoscope className="h-8 w-8 text-green-500 opacity-20" />
<Stethoscope className="h-6 sm:h-8 w-6 sm:w-8 text-green-500 opacity-20 flex-shrink-0" />
</div>
</div>
<div className="bg-card p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Consultas Hoje</h3>
<p className="text-3xl font-bold text-foreground mt-2">{stats.appointmentsToday}</p>
<div className="bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<h3 className="text-xs sm:text-sm font-medium text-muted-foreground truncate">Consultas Hoje</h3>
<p className="text-2xl sm:text-3xl font-bold text-foreground mt-1 sm:mt-2">{stats.appointmentsToday}</p>
</div>
<Calendar className="h-8 w-8 text-purple-500 opacity-20" />
<Calendar className="h-6 sm:h-8 w-6 sm:w-8 text-purple-500 opacity-20 flex-shrink-0" />
</div>
</div>
<div className="bg-card p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Relatórios Pendentes</h3>
<p className="text-3xl font-bold text-foreground mt-2">{pendingReports.length}</p>
<div className="bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<h3 className="text-xs sm:text-sm font-medium text-muted-foreground truncate">Relatórios Pendentes</h3>
<p className="text-2xl sm:text-3xl font-bold text-foreground mt-1 sm:mt-2">{pendingReports.length}</p>
</div>
<FileText className="h-8 w-8 text-orange-500 opacity-20" />
<FileText className="h-6 sm:h-8 w-6 sm:w-8 text-orange-500 opacity-20 flex-shrink-0" />
</div>
</div>
</div>
{/* 6. AÇÕES RÁPIDAS */}
<div className="bg-card p-6 rounded-lg border border-border">
<h2 className="text-lg font-semibold text-foreground mb-4">Ações Rápidas</h2>
<div className="flex flex-wrap gap-3">
<Button onClick={() => setShowPatientForm(true)} className="gap-2">
{/* 6. AÇÕES RÁPIDAS - Responsivo: stack em mobile, wrap em desktop */}
<div className="bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-border">
<h2 className="text-base sm:text-lg font-semibold text-foreground mb-3 sm:mb-4">Ações Rápidas</h2>
<div className="flex flex-col sm:flex-row flex-wrap gap-2 sm:gap-3">
<Button onClick={() => setShowPatientForm(true)} className="gap-2 text-sm sm:text-base w-full sm:w-auto">
<Plus className="h-4 w-4" />
Novo Paciente
<span className="hidden sm:inline">Novo Paciente</span>
<span className="sm:hidden">Paciente</span>
</Button>
<Button onClick={() => router.push('/agenda')} variant="outline" className="gap-2 hover:bg-primary! hover:text-white! transition-colors">
<Button onClick={() => router.push('/agenda')} variant="outline" className="gap-2 text-sm sm:text-base w-full sm:w-auto hover:bg-primary! hover:text-white! transition-colors">
<Calendar className="h-4 w-4" />
Novo Agendamento
<span className="hidden sm:inline">Novo Agendamento</span>
<span className="sm:hidden">Agendamento</span>
</Button>
<Button onClick={() => setShowDoctorForm(true)} variant="outline" className="gap-2 hover:bg-primary! hover:text-white! transition-colors">
<Button onClick={() => setShowDoctorForm(true)} variant="outline" className="gap-2 text-sm sm:text-base w-full sm:w-auto hover:bg-primary! hover:text-white! transition-colors">
<Stethoscope className="h-4 w-4" />
Novo Médico
<span className="hidden sm:inline">Novo Médico</span>
<span className="sm:hidden">Médico</span>
</Button>
<Button onClick={() => router.push('/dashboard/relatorios')} variant="outline" className="gap-2 hover:bg-primary! hover:text-white! transition-colors">
<Button onClick={() => router.push('/dashboard/relatorios')} variant="outline" className="gap-2 text-sm sm:text-base w-full sm:w-auto hover:bg-primary! hover:text-white! transition-colors">
<FileText className="h-4 w-4" />
Ver Relatórios
<span className="hidden sm:inline">Ver Relatórios</span>
<span className="sm:hidden">Relatórios</span>
</Button>
</div>
</div>
{/* 2. PRÓXIMAS CONSULTAS */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 bg-card p-6 rounded-lg border border-border">
<h2 className="text-lg font-semibold text-foreground mb-4">Próximas Consultas (7 dias)</h2>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 md:gap-6">
<div className="lg:col-span-2 bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-border">
<h2 className="text-base sm:text-lg font-semibold text-foreground mb-3 sm:mb-4">Próximas Consultas (7 dias)</h2>
{appointments.length > 0 ? (
<div className="space-y-3">
<div className="space-y-2 sm:space-y-3">
{appointments.map(appt => (
<div key={appt.id} className="flex items-center justify-between p-3 bg-muted rounded-lg hover:bg-muted/80 transition">
<div className="flex-1">
<p className="font-medium text-foreground">
<div key={appt.id} className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-3 sm:p-4 bg-muted rounded-lg hover:bg-muted/80 transition">
<div className="flex-1 min-w-0">
<p className="font-medium text-foreground text-sm sm:text-base truncate">
{patients.get(appt.patient_id)?.full_name || 'Paciente desconhecido'}
</p>
<p className="text-sm text-muted-foreground">
<p className="text-xs sm:text-sm text-muted-foreground truncate">
Médico: {doctors.get(appt.doctor_id)?.full_name || 'Médico desconhecido'}
</p>
<p className="text-xs text-muted-foreground mt-1">{formatDate(appt.scheduled_at)}</p>
<p className="text-[11px] sm:text-xs text-muted-foreground mt-1">{formatDate(appt.scheduled_at)}</p>
</div>
<div className="flex items-center gap-2">
{getStatusBadge(appt.status)}
@ -322,64 +326,64 @@ export default function DashboardPage() {
))}
</div>
) : (
<p className="text-muted-foreground">Nenhuma consulta agendada para os próximos 7 dias</p>
<p className="text-xs sm:text-sm text-muted-foreground">Nenhuma consulta agendada para os próximos 7 dias</p>
)}
</div>
{/* 5. RELATÓRIOS PENDENTES */}
<div className="bg-card p-6 rounded-lg border border-border">
<h2 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<FileText className="h-5 w-5" />
Relatórios Pendentes
<div className="bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-border">
<h2 className="text-base sm:text-lg font-semibold text-foreground mb-3 sm:mb-4 flex items-center gap-2">
<FileText className="h-4 sm:h-5 w-4 sm:w-5" />
<span className="truncate">Pendentes</span>
</h2>
{pendingReports.length > 0 ? (
<div className="space-y-2">
{pendingReports.map(report => (
<div key={report.id} className="p-3 bg-muted rounded-lg hover:bg-muted/80 transition cursor-pointer text-sm">
<div key={report.id} className="p-2 sm:p-3 bg-muted rounded-lg hover:bg-muted/80 transition cursor-pointer text-xs sm:text-sm">
<p className="font-medium text-foreground truncate">{report.order_number}</p>
<p className="text-xs text-muted-foreground">{report.exam || 'Sem descrição'}</p>
<p className="text-[10px] sm:text-xs text-muted-foreground truncate">{report.exam || 'Sem descrição'}</p>
</div>
))}
<Button onClick={() => router.push('/dashboard/relatorios')} variant="ghost" className="w-full mt-2 hover:bg-primary! hover:text-white! transition-colors" size="sm">
<Button onClick={() => router.push('/dashboard/relatorios')} variant="ghost" className="w-full mt-2 hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm" size="sm">
Ver Todos
</Button>
</div>
) : (
<p className="text-muted-foreground text-sm">Sem relatórios pendentes</p>
<p className="text-xs sm:text-sm text-muted-foreground">Sem relatórios pendentes</p>
)}
</div>
</div>
{/* 4. NOVOS USUÁRIOS */}
<div className="bg-card p-6 rounded-lg border border-border">
<h2 className="text-lg font-semibold text-foreground mb-4">Novos Usuários (últimos 7 dias)</h2>
<div className="bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-border">
<h2 className="text-base sm:text-lg font-semibold text-foreground mb-3 sm:mb-4">Novos Usuários (últimos 7 dias)</h2>
{newUsers.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 sm:gap-3">
{newUsers.map(user => (
<div key={user.id} className="p-3 bg-muted rounded-lg">
<p className="font-medium text-foreground truncate">{user.full_name || 'Sem nome'}</p>
<p className="text-sm text-muted-foreground truncate">{user.email}</p>
<div key={user.id} className="p-2 sm:p-3 bg-muted rounded-lg">
<p className="font-medium text-foreground text-xs sm:text-sm truncate">{user.full_name || 'Sem nome'}</p>
<p className="text-[10px] sm:text-xs text-muted-foreground truncate">{user.email}</p>
</div>
))}
</div>
) : (
<p className="text-muted-foreground">Nenhum novo usuário nos últimos 7 dias</p>
<p className="text-xs sm:text-sm text-muted-foreground">Nenhum novo usuário nos últimos 7 dias</p>
)}
</div>
{/* 8. ALERTAS */}
{disabledUsers.length > 0 && (
<div className="bg-card p-6 rounded-lg border border-destructive/50">
<h2 className="text-lg font-semibold text-destructive mb-4 flex items-center gap-2">
<AlertTriangle className="h-5 w-5" />
Alertas - Usuários Desabilitados
<div className="bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-destructive/50">
<h2 className="text-base sm:text-lg font-semibold text-destructive mb-3 sm:mb-4 flex items-center gap-2">
<AlertTriangle className="h-4 sm:h-5 w-4 sm:w-5" />
<span className="truncate">Usuários Desabilitados</span>
</h2>
<div className="space-y-2">
{disabledUsers.map(user => (
<Alert key={user.id} variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<strong>{user.full_name}</strong> ({user.email}) está desabilitado
<Alert key={user.id} variant="destructive" className="text-xs sm:text-sm">
<AlertCircle className="h-3 sm:h-4 w-3 sm:w-4" />
<AlertDescription className="ml-2">
<strong className="truncate">{user.full_name}</strong> ({user.email}) está desabilitado
</AlertDescription>
</Alert>
))}
@ -388,12 +392,12 @@ export default function DashboardPage() {
)}
{/* 11. LINK PARA RELATÓRIOS */}
<div className="bg-linear-to-r from-blue-500/10 to-purple-500/10 p-6 rounded-lg border border-blue-500/20">
<h2 className="text-lg font-semibold text-foreground mb-2">Seção de Relatórios</h2>
<p className="text-muted-foreground text-sm mb-4">
<div className="bg-linear-to-r from-blue-500/10 to-purple-500/10 p-4 sm:p-5 md:p-6 rounded-lg border border-blue-500/20">
<h2 className="text-base sm:text-lg font-semibold text-foreground mb-2">Seção de Relatórios</h2>
<p className="text-xs sm:text-sm text-muted-foreground mb-3 sm:mb-4">
Acesse a seção de relatórios médicos para gerenciar, visualizar e exportar documentos.
</p>
<Button asChild>
<Button asChild className="w-full sm:w-auto text-sm sm:text-base">
<Link href="/dashboard/relatorios">Ir para Relatórios</Link>
</Button>
</div>

View File

@ -478,121 +478,124 @@ export default function DoutoresPage() {
}
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center justify-between gap-4 flex-wrap">
<div className="space-y-4 sm:space-y-6 p-3 sm:p-4 md:p-6 bg-background">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl font-bold">Médicos</h1>
<p className="text-muted-foreground">Gerencie os médicos da sua clínica</p>
<h1 className="text-2xl sm:text-3xl font-bold">Médicos</h1>
<p className="text-xs sm:text-sm text-muted-foreground mt-1">Gerencie os médicos da sua clínica</p>
</div>
<div className="flex items-center gap-2 flex-wrap">
<div className="flex gap-2">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
className="pl-8 w-80"
placeholder="Digite para buscar por ID, nome, CRM ou especialidade…"
value={search}
onChange={handleSearchChange}
onKeyDown={handleSearchKeyDown}
/>
</div>
<Button
variant="secondary"
onClick={() => void handleBuscarServidor()}
disabled={loading}
className="hover:bg-primary hover:text-white"
>
Buscar
</Button>
{searchMode && (
<Button
variant="ghost"
onClick={() => {
setSearch("");
setSearchMode(false);
setSearchResults([]);
}}
>
Limpar
</Button>
)}
</div>
<Button onClick={handleAdd} disabled={loading} className="w-full sm:w-auto gap-2 text-sm sm:text-base">
<Plus className="h-4 w-4" />
Novo Médico
</Button>
</div>
{/* NOVO: Ordenar por */}
{/* Filtros e busca - Responsivos */}
<div className="flex flex-col gap-2 sm:gap-3">
{/* Linha 1: Busca + Botão buscar */}
<div className="flex gap-2">
<div className="relative flex-1 min-w-0">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
className="pl-8 w-full text-xs sm:text-sm"
placeholder="ID, nome, CRM…"
value={search}
onChange={handleSearchChange}
onKeyDown={handleSearchKeyDown}
/>
</div>
<Button
variant="secondary"
onClick={() => void handleBuscarServidor()}
disabled={loading}
className="hover:bg-primary hover:text-white text-xs sm:text-sm px-2 sm:px-4"
>
Buscar
</Button>
{searchMode && (
<Button
variant="ghost"
onClick={() => {
setSearch("");
setSearchMode(false);
setSearchResults([]);
}}
className="text-xs sm:text-sm"
>
Limpar
</Button>
)}
</div>
{/* Linha 2: Filtros */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
<select
aria-label="Ordenar por"
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
className="h-8 sm:h-9 rounded-md border border-input bg-background px-2 sm:px-3 py-1 text-xs sm:text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="name_asc">Nome (AZ)</option>
<option value="name_desc">Nome (ZA)</option>
<option value="recent">Mais recentes (carregamento)</option>
<option value="oldest">Mais antigos (carregamento)</option>
<option value="name_asc">Nome AZ</option>
<option value="name_desc">Nome ZA</option>
<option value="recent">Recentes</option>
<option value="oldest">Antigos</option>
</select>
{/* NOVO: Especialidade */}
<select
aria-label="Filtrar por especialidade"
value={specialtyFilter}
onChange={(e) => setSpecialtyFilter(e.target.value)}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
className="h-8 sm:h-9 rounded-md border border-input bg-background px-2 sm:px-3 py-1 text-xs sm:text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="">Todas as especialidades</option>
<option value="">Todas espec.</option>
{specialtyOptions.map((sp) => (
<option key={sp} value={sp}>{sp}</option>
))}
</select>
{/* NOVO: Estado (UF) */}
<select
aria-label="Filtrar por estado"
value={stateFilter}
onChange={(e) => { setStateFilter(e.target.value); setCityFilter(""); }}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
className="h-8 sm:h-9 rounded-md border border-input bg-background px-2 sm:px-3 py-1 text-xs sm:text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="">Todos os estados</option>
<option value="">Todos UF</option>
{stateOptions.map((uf) => (
<option key={uf} value={uf}>{uf}</option>
))}
</select>
{/* NOVO: Cidade (dependente do estado) */}
<select
aria-label="Filtrar por cidade"
value={cityFilter}
onChange={(e) => setCityFilter(e.target.value)}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
className="h-8 sm:h-9 rounded-md border border-input bg-background px-2 sm:px-3 py-1 text-xs sm:text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="">Todas as cidades</option>
<option value="">Todas cidades</option>
{cityOptions.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
<Button onClick={handleAdd} disabled={loading}>
<Plus className="mr-2 h-4 w-4" />
Novo Médico
</Button>
</div>
</div>
<div className="border rounded-lg overflow-hidden">
{/* Tabela para desktop (md+) */}
<div className="hidden md:block border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-primary hover:bg-primary">
<TableHead className="text-primary-foreground">Nome</TableHead>
<TableHead className="text-primary-foreground">Especialidade</TableHead>
<TableHead className="text-primary-foreground">CRM</TableHead>
<TableHead className="text-primary-foreground">Contato</TableHead>
<TableHead className="w-[100px] text-primary-foreground">Ações</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Nome</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Especialidade</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">CRM</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Contato</TableHead>
<TableHead className="w-[100px] text-primary-foreground text-xs sm:text-sm">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
<TableCell colSpan={5} className="text-center text-muted-foreground text-xs sm:text-sm">
Carregando
</TableCell>
</TableRow>
@ -687,7 +690,7 @@ export default function DoutoresPage() {
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center text-muted-foreground">
<TableCell colSpan={5} className="text-center text-muted-foreground text-xs sm:text-sm">
Nenhum médico encontrado
</TableCell>
</TableRow>
@ -696,64 +699,126 @@ export default function DoutoresPage() {
</Table>
</div>
{/* Controles de paginação */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Itens por página:</span>
{/* Cards para mobile (md: hidden) */}
<div className="md:hidden space-y-2">
{loading ? (
<div className="text-center text-xs sm:text-sm text-muted-foreground">Carregando</div>
) : paginatedDoctors.length > 0 ? (
paginatedDoctors.map((doctor) => (
<div key={doctor.id} className="bg-card p-3 sm:p-4 rounded-lg border border-border shadow-sm space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="font-medium text-sm sm:text-base truncate">{doctor.full_name}</p>
<p className="text-xs sm:text-sm text-muted-foreground truncate">{doctor.crm || "Sem CRM"}</p>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="h-6 w-6 p-0 flex items-center justify-center rounded-md hover:bg-primary hover:text-white transition-colors flex-shrink-0">
<MoreHorizontal className="h-4 w-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="text-xs sm:text-sm">
<DropdownMenuItem onClick={() => handleView(doctor)}>
<Eye className="mr-2 h-3 w-3" />
Ver
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleViewAssignedPatients(doctor)}>
<Users className="mr-2 h-3 w-3" />
Pacientes
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(String(doctor.id))}>
<Edit className="mr-2 h-3 w-3" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(String(doctor.id))} className="text-destructive">
<Trash2 className="mr-2 h-3 w-3" />
Excluir
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="grid grid-cols-2 gap-2 text-[10px] sm:text-xs">
<div>
<span className="text-muted-foreground">Espec.:</span> <span className="font-medium">{doctor.especialidade || "—"}</span>
</div>
<div>
<span className="text-muted-foreground">Email:</span> <span className="font-medium truncate">{doctor.email}</span>
</div>
<div className="col-span-2">
<span className="text-muted-foreground">Tel.:</span> <span className="font-medium">{doctor.telefone || "—"}</span>
</div>
</div>
</div>
))
) : (
<div className="text-center text-xs sm:text-sm text-muted-foreground py-4">
Nenhum médico encontrado
</div>
)}
</div>
{/* Controles de paginação - Responsivos */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4 text-xs sm:text-sm">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-muted-foreground">Itens por página:</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
className="h-8 sm:h-9 rounded-md border border-input bg-background px-2 sm:px-3 py-1 text-xs sm:text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
<span className="text-sm text-muted-foreground">
<span className="text-muted-foreground text-xs sm:text-sm">
Mostrando {paginatedDoctors.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
{Math.min(currentPage * itemsPerPage, displayedDoctors.length)} de {displayedDoctors.length}
</span>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 sm:gap-2 flex-wrap justify-center sm:justify-end">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Primeira
<span className="hidden sm:inline">Primeira</span>
<span className="sm:hidden">1ª</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Anterior
<span className="hidden sm:inline">Anterior</span>
<span className="sm:hidden">«</span>
</Button>
<span className="text-sm text-muted-foreground">
Página {currentPage} de {totalPages || 1}
<span className="text-muted-foreground text-xs sm:text-sm">
Pág {currentPage} de {totalPages || 1}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Próxima
<span className="hidden sm:inline">Próxima</span>
<span className="sm:hidden">»</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Última
<span className="hidden sm:inline">Última</span>
<span className="sm:hidden">Últ</span>
</Button>
</div>
</div>

View File

@ -256,40 +256,53 @@ export default function PacientesPage() {
}
return (
<div className="space-y-6 p-6 bg-background">
<div className="flex items-center justify-between gap-4 flex-wrap">
<div className="space-y-4 sm:space-y-6 p-3 sm:p-4 md:p-6 bg-background">
{/* Header responsivo */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div>
<h1 className="text-2xl font-bold">Pacientes</h1>
<p className="text-muted-foreground">Gerencie os pacientes</p>
<h1 className="text-2xl sm:text-3xl font-bold">Pacientes</h1>
<p className="text-xs sm:text-sm text-muted-foreground">Gerencie os pacientes</p>
</div>
<Button onClick={handleAdd} className="w-full sm:w-auto">
<Plus className="mr-2 h-4 w-4" />
<span className="hidden sm:inline">Novo paciente</span>
<span className="sm:hidden">Novo</span>
</Button>
</div>
<div className="flex items-center gap-2 flex-wrap">
{/* Busca */}
<div className="relative">
{/* Filtros e busca responsivos */}
<div className="space-y-2 sm:space-y-3">
{/* Linha 1: Busca */}
<div className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
className="pl-8 w-80"
placeholder="Buscar por nome, CPF ou ID…"
className="pl-8 w-full text-xs sm:text-sm h-8 sm:h-9"
placeholder="Nome, CPF ou ID…"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()}
/>
</div>
<Button variant="secondary" onClick={() => void handleBuscarServidor()} className="hover:bg-primary hover:text-white">
Buscar
<Button variant="secondary" size="sm" onClick={() => void handleBuscarServidor()} className="hover:bg-primary hover:text-white text-xs sm:text-sm h-8 sm:h-9 px-2 sm:px-4">
<span className="hidden sm:inline">Buscar</span>
<span className="sm:hidden">Ir</span>
</Button>
</div>
{/* Linha 2: Selects responsivos em grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2">
{/* Ordenar por */}
<select
aria-label="Ordenar por"
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
className="h-8 sm:h-9 rounded-md border border-input bg-background px-2 sm:px-3 py-1 text-xs sm:text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="name_asc">Nome (AZ)</option>
<option value="name_desc">Nome (ZA)</option>
<option value="recent">Mais recentes (carregamento)</option>
<option value="oldest">Mais antigos (carregamento)</option>
<option value="name_asc">AZ</option>
<option value="name_desc">ZA</option>
<option value="recent">Recentes</option>
<option value="oldest">Antigos</option>
</select>
{/* Estado (UF) */}
@ -300,9 +313,9 @@ export default function PacientesPage() {
setStateFilter(e.target.value);
setCityFilter("");
}}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
className="h-8 sm:h-9 rounded-md border border-input bg-background px-2 sm:px-3 py-1 text-xs sm:text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="">Todos os estados</option>
<option value="">Estado</option>
{stateOptions.map((uf) => (
<option key={uf} value={uf}>{uf}</option>
))}
@ -313,42 +326,38 @@ export default function PacientesPage() {
aria-label="Filtrar por cidade"
value={cityFilter}
onChange={(e) => setCityFilter(e.target.value)}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
className="h-8 sm:h-9 rounded-md border border-input bg-background px-2 sm:px-3 py-1 text-xs sm:text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value="">Todas as cidades</option>
<option value="">Cidade</option>
{cityOptions.map((c) => (
<option key={c} value={c}>{c}</option>
))}
</select>
<Button onClick={handleAdd}>
<Plus className="mr-2 h-4 w-4" />
Novo paciente
</Button>
</div>
</div>
<div className="border rounded-lg overflow-hidden">
{/* Desktop Table - Hidden on mobile */}
<div className="hidden md:block border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-primary hover:bg-primary">
<TableHead className="text-primary-foreground">Nome</TableHead>
<TableHead className="text-primary-foreground">CPF</TableHead>
<TableHead className="text-primary-foreground">Telefone</TableHead>
<TableHead className="text-primary-foreground">Cidade</TableHead>
<TableHead className="text-primary-foreground">Estado</TableHead>
<TableHead className="w-[100px] text-primary-foreground">Ações</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Nome</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">CPF</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Telefone</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Cidade</TableHead>
<TableHead className="text-primary-foreground text-xs sm:text-sm">Estado</TableHead>
<TableHead className="w-[100px] text-primary-foreground text-xs sm:text-sm">Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedData.length > 0 ? (
paginatedData.map((p) => (
<TableRow key={p.id}>
<TableCell className="font-medium">{p.full_name || "(sem nome)"}</TableCell>
<TableCell>{p.cpf || "-"}</TableCell>
<TableCell>{p.phone_mobile || "-"}</TableCell>
<TableCell>{p.city || "-"}</TableCell>
<TableCell>{p.state || "-"}</TableCell>
<TableCell className="font-medium text-xs sm:text-sm">{p.full_name || "(sem nome)"}</TableCell>
<TableCell className="text-xs sm:text-sm">{p.cpf || "-"}</TableCell>
<TableCell className="text-xs sm:text-sm">{p.phone_mobile || "-"}</TableCell>
<TableCell className="text-xs sm:text-sm">{p.city || "-"}</TableCell>
<TableCell className="text-xs sm:text-sm">{p.state || "-"}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -381,7 +390,7 @@ export default function PacientesPage() {
))
) : (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
<TableCell colSpan={6} className="text-center text-xs sm:text-sm text-muted-foreground py-4">
Nenhum paciente encontrado
</TableCell>
</TableRow>
@ -390,64 +399,132 @@ export default function PacientesPage() {
</Table>
</div>
{/* Controles de paginação */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">Itens por página:</span>
{/* Mobile Cards - Hidden on desktop */}
<div className="md:hidden space-y-2">
{paginatedData.length > 0 ? (
paginatedData.map((p) => (
<div key={p.id} className="bg-card p-3 sm:p-4 rounded-lg border border-border hover:border-primary transition-colors">
<div className="grid grid-cols-2 gap-2">
<div className="col-span-2 flex justify-between items-start">
<div className="flex-1">
<div className="text-[10px] sm:text-xs font-semibold text-primary">Nome</div>
<div className="text-xs sm:text-sm font-medium truncate">{p.full_name || "(sem nome)"}</div>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="h-7 w-7 p-0 flex items-center justify-center rounded-md hover:bg-primary hover:text-white transition-colors flex-shrink-0">
<span className="sr-only">Menu</span>
<MoreHorizontal className="h-3.5 w-3.5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => handleView(p)}>
<Eye className="mr-2 h-4 w-4" />
Ver
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleEdit(String(p.id))}>
<Edit className="mr-2 h-4 w-4" />
Editar
</DropdownMenuItem>
<DropdownMenuItem onClick={() => handleDelete(String(p.id))} className="text-destructive">
<Trash2 className="mr-2 h-4 w-4" />
Excluir
</DropdownMenuItem>
<DropdownMenuItem onClick={() => { setAssignPatientId(String(p.id)); setAssignDialogOpen(true); }}>
<Edit className="mr-2 h-4 w-4" />
Atribuir prof.
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div>
<div className="text-[10px] sm:text-xs text-muted-foreground">CPF</div>
<div className="text-[10px] sm:text-xs font-medium">{p.cpf || "-"}</div>
</div>
<div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Telefone</div>
<div className="text-[10px] sm:text-xs font-medium">{p.phone_mobile || "-"}</div>
</div>
<div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Cidade</div>
<div className="text-[10px] sm:text-xs font-medium truncate">{p.city || "-"}</div>
</div>
<div>
<div className="text-[10px] sm:text-xs text-muted-foreground">Estado</div>
<div className="text-[10px] sm:text-xs font-medium">{p.state || "-"}</div>
</div>
</div>
</div>
))
) : (
<div className="text-center text-xs sm:text-sm text-muted-foreground py-4">
Nenhum paciente encontrado
</div>
)}
</div>
{/* Controles de paginação - Responsivos */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4 text-xs sm:text-sm">
<div className="flex items-center gap-2 flex-wrap">
<span className="text-muted-foreground text-xs sm:text-sm">Itens por página:</span>
<select
value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))}
className="h-9 rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
className="h-8 sm:h-9 rounded-md border border-input bg-background px-2 sm:px-3 py-1 text-xs sm:text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-primary hover:border-primary transition-colors cursor-pointer"
>
<option value={10}>10</option>
<option value={15}>15</option>
<option value={20}>20</option>
</select>
<span className="text-sm text-muted-foreground">
<span className="text-muted-foreground text-xs sm:text-sm">
Mostrando {paginatedData.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
{Math.min(currentPage * itemsPerPage, filtered.length)} de {filtered.length}
</span>
</div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 sm:gap-2 flex-wrap justify-center sm:justify-end">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(1)}
disabled={currentPage === 1}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Primeira
<span className="hidden sm:inline">Primeira</span>
<span className="sm:hidden">1ª</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Anterior
<span className="hidden sm:inline">Anterior</span>
<span className="sm:hidden">«</span>
</Button>
<span className="text-sm text-muted-foreground">
Página {currentPage} de {totalPages || 1}
<span className="text-muted-foreground text-xs sm:text-sm">
Pág {currentPage} de {totalPages || 1}
</span>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Próxima
<span className="hidden sm:inline">Próxima</span>
<span className="sm:hidden">»</span>
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0}
className="hover:bg-primary! hover:text-white! transition-colors"
className="hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm h-7 sm:h-9 px-1 sm:px-3"
>
Última
<span className="hidden sm:inline">Última</span>
<span className="sm:hidden">Últ</span>
</Button>
</div>
</div>

View File

@ -866,11 +866,15 @@ const ProfissionalPage = () => {
{appointment.type}
</div>
</div>
<div className="hidden md:flex items-center justify-end">
<div className="relative group">
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2 px-2 py-1 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 text-xs rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-50">
Ver informações do paciente
<div className="absolute top-full left-1/2 transform -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent border-t-gray-900 dark:border-t-gray-100"></div>
<div className="flex items-center justify-end">
{/* Tornar o trigger focusable/touchable para mobile: tabIndex + classes responsivas */}
<div className="relative group" tabIndex={0} role="button" aria-label={`Informações da consulta ${appointment.title}`}>
{/* Tooltip: em telas pequenas com scrollbar horizontal permanente (overflow-x-scroll); desktop sem scroll (hover) */}
<div className="absolute sm:bottom-full bottom-auto sm:left-1/2 left-0 sm:transform sm:-translate-x-1/2 right-0 sm:mb-2 top-full sm:top-auto sm:mt-0 mt-2 px-3 py-2 bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900 text-xs sm:text-xs rounded-md opacity-0 group-hover:opacity-100 group-focus:opacity-100 transition-opacity duration-200 pointer-events-auto sm:pointer-events-none whitespace-normal sm:whitespace-nowrap z-50 max-w-[92vw] sm:max-w-xs overflow-y-hidden overflow-x-scroll sm:overflow-x-hidden">
<div className="font-medium">{appointment.title} {appointment.type}</div>
<div className="text-[11px] text-muted-foreground mt-1">Agendamento para {appointment.title}. Status: {appointment.type === 'Rotina' ? 'requested' : appointment.type}.</div>
<div className="text-[11px] text-muted-foreground mt-1">{appointment.time} {appointment.date}</div>
<div className="absolute sm:top-full top-auto sm:left-1/2 left-1/2 transform sm:-translate-x-1/2 -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-t-4 border-transparent sm:border-t-gray-900 dark:sm:border-t-gray-100 sm:mt-0 mt-0"></div>
</div>
</div>
</div>
@ -2252,9 +2256,9 @@ const ProfissionalPage = () => {
</div>
{/* Content */}
<div className="flex-1 overflow-hidden flex">
{/* Left Panel */}
<div className="flex-1 flex flex-col">
<div className="flex-1 overflow-hidden flex flex-row">
{/* Left Panel - Editor */}
<div className={`flex flex-col overflow-hidden transition-all ${showPreview ? 'w-1/2 sm:w-3/5' : 'w-full'}`}>
{/* 'Informações' section removed to keep editor-only experience */}
{activeTab === "editor" && (
@ -2456,54 +2460,52 @@ const ProfissionalPage = () => {
{/* Preview Panel */}
{showPreview && (
<div className="hidden md:flex md:w-1/2 border-l border-border bg-muted/20 flex-col">
<div className="p-2 md:p-2.5 border-b border-border flex-shrink-0">
<h3 className="font-semibold text-xs md:text-sm text-foreground">Pré-visualização do Laudo</h3>
<div className="w-1/2 sm:w-2/5 border-l border-border bg-muted/20 flex flex-col overflow-hidden">
<div className="p-1 sm:p-1.5 border-b border-border flex-shrink-0">
<h3 className="font-semibold text-xs sm:text-sm text-foreground line-clamp-1">Pré-visualização</h3>
</div>
<div className="flex-1 overflow-y-auto p-1.5 md:p-2">
<div className="bg-background border border-border rounded p-2 md:p-2.5 text-xs">
<div className="flex-1 overflow-y-auto p-1 sm:p-1.5">
<div className="bg-background border border-border rounded p-1 sm:p-1.5 text-xs space-y-1">
{/* Header do Laudo */}
<div className="text-center mb-2 pb-2 border-b border-border/40">
<h2 className="text-sm font-bold leading-tight">
LAUDO MÉDICO {campos.especialidade ? `- ${campos.especialidade.toUpperCase()}` : ''}
<div className="text-center mb-1 pb-1 border-b border-border/40">
<h2 className="text-xs sm:text-sm font-bold leading-tight line-clamp-2">
LAUDO {campos.especialidade ? `- ${campos.especialidade.toUpperCase().substring(0, 15)}` : ''}
</h2>
{campos.exame && (
<p className="text-xs font-semibold mt-1">{campos.exame}</p>
<p className="text-xs font-semibold mt-0.5 line-clamp-1">{campos.exame.substring(0, 30)}</p>
)}
{campos.mostrarData && (
<p className="text-xs text-muted-foreground mt-0.5">
Data: {new Date().toLocaleDateString('pt-BR')}
{new Date().toLocaleDateString('pt-BR')}
</p>
)}
</div>
{/* Dados do Paciente - Compacto */}
{/* Dados do Paciente - Ultra Compacto */}
{(isNewLaudo ? pacienteSelecionado : laudo?.paciente) && (
<div className="mb-2 pb-2 border-b border-border/40 space-y-0.5">
<div><span className="font-semibold">Paciente:</span> {isNewLaudo ? getPatientName(pacienteSelecionado) : getPatientName(laudo?.paciente)}</div>
<div><span className="font-semibold">CPF:</span> {isNewLaudo ? getPatientCpf(pacienteSelecionado) : getPatientCpf(laudo?.paciente)}</div>
<div><span className="font-semibold">Idade:</span> {isNewLaudo ? getPatientAge(pacienteSelecionado) : getPatientAge(laudo?.paciente)} anos</div>
<div><span className="font-semibold">Sexo:</span> {isNewLaudo ? getPatientSex(pacienteSelecionado) : getPatientSex(laudo?.paciente)}</div>
<div className="mb-1 pb-1 border-b border-border/40 space-y-0">
<div className="text-xs"><span className="font-semibold">Pac:</span> {(isNewLaudo ? getPatientName(pacienteSelecionado) : getPatientName(laudo?.paciente)).substring(0, 15)}</div>
<div className="text-xs"><span className="font-semibold">CPF:</span> {(isNewLaudo ? getPatientCpf(pacienteSelecionado) : getPatientCpf(laudo?.paciente)).substring(0, 10)}</div>
</div>
)}
{/* Informações Clínicas */}
<div className="mb-2 pb-2 border-b border-border/40 space-y-0.5">
<div className="mb-1 pb-1 border-b border-border/40 space-y-0">
{campos.cid && (
<div><span className="font-semibold">CID:</span> <span className="text-blue-600 dark:text-blue-400 font-semibold">{campos.cid}</span></div>
<div className="text-xs"><span className="font-semibold">CID:</span> <span className="text-blue-600 dark:text-blue-400 font-semibold">{campos.cid}</span></div>
)}
{campos.diagnostico && (
<div><span className="font-semibold">Diagnóstico:</span> {campos.diagnostico}</div>
<div className="text-xs line-clamp-2"><span className="font-semibold">Diag:</span> {campos.diagnostico.substring(0, 40)}</div>
)}
</div>
{/* Conteúdo Principal */}
{content && (
<div className="mb-2 pb-2 border-b border-border/40">
<div className="mb-1 pb-1 border-b border-border/40">
<div
className="text-xs leading-normal [&_p]:my-0.5 [&_br]:mb-0.5 whitespace-pre-wrap"
className="text-xs leading-tight [&_p]:my-0 [&_br]:mb-0 whitespace-pre-wrap line-clamp-4"
dangerouslySetInnerHTML={{
__html: processContent(content)
__html: processContent(content).substring(0, 150)
}}
/>
</div>
@ -2511,28 +2513,8 @@ const ProfissionalPage = () => {
{/* Conclusão */}
{campos.conclusao && (
<div className="mb-2 pb-2 border-b border-border/40">
<div><span className="font-semibold">Conclusão:</span></div>
<div className="text-xs leading-normal mt-0.5">{campos.conclusao}</div>
</div>
)}
{/* Imagens Compactas */}
{imagens.length > 0 && (
<div className="mb-2 pb-2 border-b border-border/40">
<div className="font-semibold mb-1">Imagens:</div>
<div className="grid grid-cols-2 gap-1">
{imagens.map((img) => (
// eslint-disable-next-line @next/next/no-img-element
<img
key={img.id}
src={img.url}
alt={img.name}
className="w-full h-14 object-cover rounded border border-border/40"
title={img.name}
/>
))}
</div>
<div className="mb-1 pb-1 border-b border-border/40">
<div className="text-xs line-clamp-2"><span className="font-semibold">Conc:</span> {campos.conclusao.substring(0, 40)}</div>
</div>
)}
@ -2541,14 +2523,14 @@ const ProfissionalPage = () => {
<div className="pt-1 mt-1 border-t border-border/40 text-center">
{assinaturaImg && assinaturaImg.length > 30 ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={assinaturaImg} alt="Assinatura Digital" className="mx-auto h-8 object-contain mb-0.5" />
<img src={assinaturaImg} alt="Assinatura Digital" className="mx-auto h-6 object-contain mb-0.5" />
) : (
<div className="text-xs text-muted-foreground italic">Assinatura pendente</div>
<div className="text-xs text-muted-foreground italic">Sem assinatura</div>
)}
<div className="border-t border-border/40 my-0.5"></div>
<p className="text-xs font-semibold">{((profileData as any)?.nome || (profileData as any)?.nome_social) || user?.name || 'Squad-20'}</p>
<p className="text-xs font-semibold leading-tight">{((profileData as any)?.nome || (profileData as any)?.nome_social || user?.name || 'Squad-20').substring(0, 20)}</p>
{(((profileData as any)?.crm) || ((user?.profile as any)?.crm)) ? (
<p className="text-xs text-muted-foreground">CRM {(((profileData as any)?.crm) || (user?.profile as any)?.crm).toString().replace(/^(?:CRM\s*)+/i, '').trim()}</p>
<p className="text-xs text-muted-foreground">CRM {(((profileData as any)?.crm) || (user?.profile as any)?.crm).toString().replace(/^(?:CRM\s*)+/i, '').trim().substring(0, 10)}</p>
) : null}
</div>
)}

View File

@ -936,15 +936,29 @@ export function CalendarRegistrationForm({ formData, onFormChange, createMode =
<div className="absolute top-full left-0 mt-1 z-50 bg-card border border-border rounded-md shadow-lg p-3">
<CalendarComponent
mode="single"
selected={formData.appointmentDate ? new Date(formData.appointmentDate + 'T00:00:00') : undefined}
selected={formData.appointmentDate ? (() => {
try {
const [y, m, d] = String(formData.appointmentDate).split('-').map(Number);
return new Date(y, m - 1, d);
} catch (e) {
return undefined;
}
})() : undefined}
onSelect={(date) => {
if (date) {
const dateStr = date.toISOString().split('T')[0];
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const dateStr = `${y}-${m}-${d}`;
onFormChange({ ...formData, appointmentDate: dateStr });
setShowDatePicker(false);
}
}}
disabled={(date) => date < new Date(new Date().toISOString().split('T')[0] + 'T00:00:00')}
disabled={(date) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
return date < today;
}}
/>
</div>
)}

View File

@ -1,7 +1,7 @@
"use client";
import { useEffect, useMemo, useState } from "react";
import { parse } from 'date-fns';
import { parse, parseISO, format } from 'date-fns';
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
@ -11,9 +11,11 @@ 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 { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload, User, X, XCircle, Trash2 } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
import { cn } from "@/lib/utils";
import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload, User, X, XCircle, Trash2, CalendarIcon } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import {
criarMedico,
atualizarMedico,
@ -76,7 +78,7 @@ type FormData = {
cpf: string;
rg: string;
sexo: string;
data_nascimento: string;
data_nascimento: Date | null;
email: string;
telefone: string;
celular: string;
@ -109,7 +111,7 @@ const initial: FormData = {
cpf: "",
rg: "",
sexo: "",
data_nascimento: "",
data_nascimento: null,
email: "",
telefone: "",
celular: "", // Aqui, 'celular' pode ser 'phone_mobile'
@ -150,7 +152,7 @@ export function DoctorRegistrationForm({
}: DoctorRegistrationFormProps) {
const [form, setForm] = useState<FormData>(initial);
const [errors, setErrors] = useState<Record<string, string>>({});
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false, formacao: false, admin: false });
const [expanded, setExpanded] = useState({ dados: true, contato: false, endereco: false, obs: false, formacao: false });
const [isSubmitting, setSubmitting] = useState(false);
const [isUploadingPhoto, setUploadingPhoto] = useState(false);
const [isSearchingCEP, setSearchingCEP] = useState(false);
@ -257,7 +259,7 @@ export function DoctorRegistrationForm({
cpf: String(m.cpf || ""),
rg: String(m.rg || m.document_number || ""),
sexo: normalizeSex(m.sexo || m.sex || m.sexualidade || null) ?? "",
data_nascimento: String(formatBirth(m.data_nascimento || m.birth_date || m.birthDate || "")),
data_nascimento: m.data_nascimento ? parseISO(String(m.data_nascimento)) : m.birth_date ? parseISO(String(m.birth_date)) : null,
email: String(m.email || ""),
telefone: String(m.telefone || m.phone_mobile || m.phone || m.mobile || ""),
celular: String(m.celular || m.phone2 || ""),
@ -430,36 +432,6 @@ function setField<T extends keyof FormData>(k: T, v: FormData[T]) {
}
function toPayload(): MedicoInput {
// Converte data de nascimento para ISO (yyyy-MM-dd) tentando vários formatos
let isoDate: string | null = null;
try {
const raw = String(form.data_nascimento || '').trim();
if (raw) {
const formats = ['dd/MM/yyyy', 'dd-MM-yyyy', 'yyyy-MM-dd', 'MM/dd/yyyy'];
for (const f of formats) {
try {
const d = parse(raw, f, new Date());
if (!isNaN(d.getTime())) {
isoDate = d.toISOString().slice(0, 10);
break;
}
} catch (e) {
// ignore and try next
}
}
if (!isoDate) {
const parts = raw.split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const [d, m, y] = parts;
const date = new Date(Number(y), Number(m) - 1, Number(d));
if (!isNaN(date.getTime())) isoDate = date.toISOString().slice(0, 10);
}
}
}
} catch (err) {
console.debug('[DoctorForm] parse data_nascimento failed:', form.data_nascimento, err);
}
return {
user_id: null,
crm: form.crm || "",
@ -477,7 +449,7 @@ function toPayload(): MedicoInput {
neighborhood: form.bairro || undefined,
city: form.cidade || "",
state: form.estado || "",
birth_date: isoDate,
birth_date: form.data_nascimento ? form.data_nascimento.toISOString().slice(0, 10) : null,
rg: form.rg || null,
active: true,
created_by: null,
@ -796,7 +768,7 @@ async function handleSubmit(ev: React.FormEvent) {
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-4">
<div className="space-y-2">
<Label>Sexo</Label>
<Select value={form.sexo} onValueChange={(v) => setField("sexo", v)}>
@ -811,23 +783,29 @@ async function handleSubmit(ev: React.FormEvent) {
</Select>
</div>
<div className="space-y-2">
<Label>Data de Nascimento</Label>
<Input
placeholder="dd/mm/aaaa"
value={form.data_nascimento}
onChange={(e) => {
const v = e.target.value.replace(/[^0-9\/]/g, "").slice(0, 10);
setField("data_nascimento", v);
}}
onBlur={() => {
const raw = form.data_nascimento;
const parts = raw.split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const d = `${parts[0].padStart(2,'0')}/${parts[1].padStart(2,'0')}/${parts[2].padStart(4,'0')}`;
setField("data_nascimento", d);
}
}}
/>
<Label htmlFor="data_nascimento_input">Data de Nascimento</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!form.data_nascimento && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{form.data_nascimento ? format(form.data_nascimento, "dd/MM/yyyy") : <span>Selecione uma data</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={form.data_nascimento}
onSelect={(date) => setField("data_nascimento", date || null)}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
</div>
</CardContent>
@ -949,98 +927,6 @@ async function handleSubmit(ev: React.FormEvent) {
</Card>
</Collapsible>
<Collapsible open={expanded.admin} onOpenChange={() => setExpanded((s) => ({ ...s, admin: !s.admin }))}>
<Card>
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer hover:bg-muted/50 transition-colors">
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<User className="h-4 w-4" />
Dados Administrativos e Financeiros
</span>
{expanded.admin ? <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>Tipo de Vínculo</Label>
<Select value={form.tipo_vinculo} onValueChange={(v) => setField("tipo_vinculo", v)}>
<SelectTrigger>
<SelectValue placeholder="Selecione o vínculo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="funcionario">Funcionário</SelectItem>
<SelectItem value="autonomo">Autônomo</SelectItem>
<SelectItem value="parceiro">Parceiro</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Valor da Consulta</Label>
<Input
type="number"
value={form.valor_consulta}
onChange={(e) => setField("valor_consulta", e.target.value)}
placeholder="R$ 0,00"
/>
</div>
</div>
{/* Agenda/Horário removido conforme solicitado */}
<div className="space-y-4">
<Label>Dados Bancários</Label>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Banco</Label>
<Input
value={form.dados_bancarios.banco}
onChange={(e) => setField("dados_bancarios", { ...form.dados_bancarios, banco: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>Agência</Label>
<Input
value={form.dados_bancarios.agencia}
onChange={(e) => setField("dados_bancarios", { ...form.dados_bancarios, agencia: e.target.value })}
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Conta</Label>
<Input
value={form.dados_bancarios.conta}
onChange={(e) => setField("dados_bancarios", { ...form.dados_bancarios, conta: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>Tipo de Conta</Label>
<Select
value={form.dados_bancarios.tipo_conta}
onValueChange={(v) => setField("dados_bancarios", { ...form.dados_bancarios, tipo_conta: v })}
>
<SelectTrigger>
<SelectValue placeholder="Selecione o tipo" />
</SelectTrigger>
<SelectContent>
<SelectItem value="corrente">Conta Corrente</SelectItem>
<SelectItem value="poupanca">Conta Poupança</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</CardContent>
</CollapsibleContent>
</Card>
</Collapsible>
<Collapsible open={expanded.endereco} onOpenChange={() => setExpanded((s) => ({ ...s, endereco: !s.endereco }))}>
<Card>
<CollapsibleTrigger asChild>

View File

@ -11,7 +11,10 @@ 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 { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload, User, X, XCircle, Trash2 } from "lucide-react";
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
import { cn } from "@/lib/utils";
import { AlertCircle, ChevronDown, ChevronUp, FileImage, Loader2, Save, Upload, User, X, XCircle, Trash2, CalendarIcon } from "lucide-react";
import {
Paciente,
@ -51,7 +54,7 @@ type FormData = {
cpf: string;
rg: string;
sexo: string;
birth_date: string;
birth_date: Date | null;
email: string;
telefone: string;
cep: string;
@ -72,7 +75,7 @@ const initial: FormData = {
cpf: "",
rg: "",
sexo: "",
birth_date: "",
birth_date: null,
email: "",
telefone: "",
cep: "",
@ -150,7 +153,7 @@ export function PatientRegistrationForm({
cpf: p.cpf || "",
rg: p.rg || "",
sexo: p.sex || "",
birth_date: p.birth_date ? (() => { try { return format(parseISO(String(p.birth_date)), 'dd/MM/yyyy'); } catch { return String(p.birth_date); } })() : "",
birth_date: p.birth_date ? parseISO(String(p.birth_date)) : null,
telefone: p.phone_mobile || "",
email: p.email || "",
cep: p.cep || "",
@ -212,44 +215,13 @@ export function PatientRegistrationForm({
}
function toPayload(): PacienteInput {
let isoDate: string | null = null;
try {
const raw = String(form.birth_date || '').trim();
if (raw) {
// Try common formats first
const formats = ['dd/MM/yyyy', 'dd-MM-yyyy', 'yyyy-MM-dd', 'MM/dd/yyyy'];
for (const f of formats) {
try {
const d = parse(raw, f, new Date());
if (!isNaN(d.getTime())) {
isoDate = d.toISOString().slice(0, 10);
break;
}
} catch (e) {
// ignore and try next format
}
}
// Fallback: split numeric parts (handles 'dd mm yyyy' or 'ddmmyyyy' with separators)
if (!isoDate) {
const parts = raw.split(/\D+/).filter(Boolean);
if (parts.length === 3) {
const [d, m, y] = parts;
const date = new Date(Number(y), Number(m) - 1, Number(d));
if (!isNaN(date.getTime())) isoDate = date.toISOString().slice(0, 10);
}
}
}
} catch (err) {
console.debug('[PatientForm] parse birth_date failed:', form.birth_date, err);
}
return {
full_name: form.nome,
social_name: form.nome_social || null,
cpf: form.cpf,
rg: form.rg || null,
sex: form.sexo || null,
birth_date: isoDate,
birth_date: form.birth_date ? form.birth_date.toISOString().slice(0, 10) : null,
phone_mobile: form.telefone || null,
email: form.email || null,
cep: form.cep || null,
@ -376,7 +348,7 @@ export function PatientRegistrationForm({
<CardContent className="space-y-4">
<div className="flex items-center gap-4">
<div className="w-24 h-24 border-2 border-dashed border-muted-foreground 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" />}
{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 rounded-md transition-colors">
@ -399,14 +371,43 @@ export function PatientRegistrationForm({
<div className="space-y-2"><Label>RG</Label><Input value={form.rg} onChange={(e) => setField("rg", formatRG(e.target.value))} placeholder="00.000.000-0" maxLength={12} /></div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2"><Label>Sexo</Label>
<div className="space-y-4">
<div className="space-y-2">
<Label>Sexo</Label>
<Select value={form.sexo} onValueChange={(v) => setField("sexo", v)}>
<SelectTrigger><SelectValue placeholder="Selecione o sexo" /></SelectTrigger>
<SelectContent><SelectItem value="masculino">Masculino</SelectItem><SelectItem value="feminino">Feminino</SelectItem><SelectItem value="outro">Outro</SelectItem></SelectContent>
<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>Data de Nascimento</Label><Input placeholder="dd/mm/aaaa" value={form.birth_date} onChange={(e) => setField("birth_date", formatDataNascimento(e.target.value))} maxLength={10} /></div>
<div className="space-y-2">
<Label htmlFor="birth_date_input">Data de Nascimento</Label>
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!form.birth_date && "text-muted-foreground"
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{form.birth_date ? format(form.birth_date as Date, "dd/MM/yyyy") : <span>Selecione uma data</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={form.birth_date ?? undefined}
onSelect={(date) => setField("birth_date", date || null)}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
</div>
</CardContent>
</CollapsibleContent>

View File

@ -872,7 +872,8 @@ function MonthView({
}
return (
<Card className="overflow-hidden">
// Permitir que popovers absolutos saiam do grid do mês sem serem cortados
<Card className="overflow-visible">
<div className="grid grid-cols-7 border-b">
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => (
<div key={day} className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm">