develop #83

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

View File

@ -469,76 +469,78 @@ export default function ConsultasPage() {
} }
return ( 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="flex items-center justify-between gap-4 flex-wrap"> {/* Header responsivo */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div> <div>
<h1 className="text-2xl font-bold">Gerenciamento de Consultas</h1> <h1 className="text-2xl sm:text-3xl font-bold">Consultas</h1>
<p className="text-muted-foreground">Visualize, filtre e gerencie todas as consultas da clínica.</p> <p className="text-xs sm:text-sm text-muted-foreground">Gerencie todas as consultas da clínica</p>
</div> </div>
<div className="flex items-center gap-2"> <Link href="/agenda?origin=consultas">
{/* Pass origin so the Agenda page can return to Consultas when cancelling */} <Button className="w-full sm:w-auto h-8 sm:h-9 gap-1 bg-blue-600 text-xs sm:text-sm">
<Link href="/agenda?origin=consultas"> <PlusCircle className="h-3.5 w-3.5" />
<Button size="sm" className="h-8 gap-1 bg-blue-600"> <span className="hidden sm:inline">Agendar</span>
<PlusCircle className="h-3.5 w-3.5" /> <span className="sm:hidden">Nova</span>
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">Agendar Nova Consulta</span> </Button>
</Button> </Link>
</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>
</div> </div>
<Card> {/* Loading state */}
<CardHeader> {isLoading ? (
<CardTitle>Consultas Agendadas</CardTitle> <div className="w-full py-12 flex justify-center items-center border rounded-lg">
<CardDescription>Visualize, filtre e gerencie todas as consultas da clínica.</CardDescription> <Loader2 className="animate-spin mr-2" />
<div className="pt-4 flex flex-wrap items-center gap-4"> <span className="text-xs sm:text-sm">Carregando agendamentos...</span>
<div className="flex-1 min-w-[250px] flex gap-2 items-center"> </div>
<div className="relative flex-1"> ) : (
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> <>
<Input {/* Desktop Table - Hidden on mobile */}
type="search" <div className="hidden md:block border rounded-lg overflow-hidden">
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>
) : (
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-primary hover:bg-primary"> <TableRow className="bg-primary hover:bg-primary">
<TableHead className="text-primary-foreground">Paciente</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Paciente</TableHead>
<TableHead className="text-primary-foreground">Médico</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Médico</TableHead>
<TableHead className="text-primary-foreground">Status</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Status</TableHead>
<TableHead className="text-primary-foreground">Data e Hora</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Data e Hora</TableHead>
<TableHead className="text-primary-foreground">Ações</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Ações</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{paginatedAppointments.map((appointment) => { {paginatedAppointments.map((appointment) => {
// appointment.professional may now contain the doctor's name (resolved)
const professionalLookup = mockProfessionals.find((p) => p.id === appointment.professional); const professionalLookup = mockProfessionals.find((p) => p.id === appointment.professional);
const professionalName = typeof appointment.professional === "string" && appointment.professional && !professionalLookup const professionalName = typeof appointment.professional === "string" && appointment.professional && !professionalLookup
? appointment.professional ? appointment.professional
@ -546,8 +548,8 @@ export default function ConsultasPage() {
return ( return (
<TableRow key={appointment.id}> <TableRow key={appointment.id}>
<TableCell className="font-medium">{appointment.patient}</TableCell> <TableCell className="font-medium text-xs sm:text-sm">{appointment.patient}</TableCell>
<TableCell>{professionalName}</TableCell> <TableCell className="text-xs sm:text-sm">{professionalName}</TableCell>
<TableCell> <TableCell>
<Badge <Badge
variant={ variant={
@ -562,12 +564,12 @@ export default function ConsultasPage() {
{capitalize(appointment.status)} {capitalize(appointment.status)}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell>{formatDate(appointment.scheduled_at ?? appointment.time)}</TableCell> <TableCell className="text-xs sm:text-sm">{formatDate(appointment.scheduled_at ?? appointment.time)}</TableCell>
<TableCell> <TableCell>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <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"> <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" /> <MoreHorizontal className="h-4 w-4" />
</button> </button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@ -592,68 +594,146 @@ export default function ConsultasPage() {
})} })}
</TableBody> </TableBody>
</Table> </Table>
)} </div>
</CardContent>
</Card>
{/* Controles de paginação */} {/* Mobile Cards - Hidden on desktop */}
<div className="flex items-center justify-between"> <div className="md:hidden space-y-2">
<div className="flex items-center gap-2"> {paginatedAppointments.length > 0 ? (
<span className="text-sm text-muted-foreground">Itens por página:</span> 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 <select
value={itemsPerPage} value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))} 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={10}>10</option>
<option value={15}>15</option> <option value={15}>15</option>
<option value={20}>20</option> <option value={20}>20</option>
</select> </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{" "} Mostrando {paginatedAppointments.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
{Math.min(currentPage * itemsPerPage, appointments.length)} de {appointments.length} {Math.min(currentPage * itemsPerPage, appointments.length)} de {appointments.length}
</span> </span>
</div> </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 <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage(1)} onClick={() => setCurrentPage(1)}
disabled={currentPage === 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>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))} onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 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> </Button>
<span className="text-sm text-muted-foreground"> <span className="text-muted-foreground text-xs sm:text-sm">
Página {currentPage} de {totalPages || 1} Pág {currentPage} de {totalPages || 1}
</span> </span>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))} onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0} 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>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage(totalPages)} onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0} 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> </Button>
</div> </div>
</div> </div>

View File

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

View File

@ -478,121 +478,124 @@ export default function DoutoresPage() {
} }
return ( 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="flex items-center justify-between gap-4 flex-wrap"> <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div> <div>
<h1 className="text-2xl font-bold">Médicos</h1> <h1 className="text-2xl sm:text-3xl font-bold">Médicos</h1>
<p className="text-muted-foreground">Gerencie os médicos da sua clínica</p> <p className="text-xs sm:text-sm text-muted-foreground mt-1">Gerencie os médicos da sua clínica</p>
</div> </div>
<div className="flex items-center gap-2 flex-wrap"> <Button onClick={handleAdd} disabled={loading} className="w-full sm:w-auto gap-2 text-sm sm:text-base">
<div className="flex gap-2"> <Plus className="h-4 w-4" />
<div className="relative"> Novo Médico
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" /> </Button>
<Input </div>
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>
{/* 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 <select
aria-label="Ordenar por" aria-label="Ordenar por"
value={sortBy} value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)} 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_asc">Nome AZ</option>
<option value="name_desc">Nome (ZA)</option> <option value="name_desc">Nome ZA</option>
<option value="recent">Mais recentes (carregamento)</option> <option value="recent">Recentes</option>
<option value="oldest">Mais antigos (carregamento)</option> <option value="oldest">Antigos</option>
</select> </select>
{/* NOVO: Especialidade */}
<select <select
aria-label="Filtrar por especialidade" aria-label="Filtrar por especialidade"
value={specialtyFilter} value={specialtyFilter}
onChange={(e) => setSpecialtyFilter(e.target.value)} 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) => ( {specialtyOptions.map((sp) => (
<option key={sp} value={sp}>{sp}</option> <option key={sp} value={sp}>{sp}</option>
))} ))}
</select> </select>
{/* NOVO: Estado (UF) */}
<select <select
aria-label="Filtrar por estado" aria-label="Filtrar por estado"
value={stateFilter} value={stateFilter}
onChange={(e) => { setStateFilter(e.target.value); setCityFilter(""); }} 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) => ( {stateOptions.map((uf) => (
<option key={uf} value={uf}>{uf}</option> <option key={uf} value={uf}>{uf}</option>
))} ))}
</select> </select>
{/* NOVO: Cidade (dependente do estado) */}
<select <select
aria-label="Filtrar por cidade" aria-label="Filtrar por cidade"
value={cityFilter} value={cityFilter}
onChange={(e) => setCityFilter(e.target.value)} 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) => ( {cityOptions.map((c) => (
<option key={c} value={c}>{c}</option> <option key={c} value={c}>{c}</option>
))} ))}
</select> </select>
<Button onClick={handleAdd} disabled={loading}>
<Plus className="mr-2 h-4 w-4" />
Novo Médico
</Button>
</div> </div>
</div> </div>
<div className="border rounded-lg overflow-hidden"> {/* Tabela para desktop (md+) */}
<div className="hidden md:block border rounded-lg overflow-hidden">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-primary hover:bg-primary"> <TableRow className="bg-primary hover:bg-primary">
<TableHead className="text-primary-foreground">Nome</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Nome</TableHead>
<TableHead className="text-primary-foreground">Especialidade</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Especialidade</TableHead>
<TableHead className="text-primary-foreground">CRM</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">CRM</TableHead>
<TableHead className="text-primary-foreground">Contato</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Contato</TableHead>
<TableHead className="w-[100px] text-primary-foreground">Ações</TableHead> <TableHead className="w-[100px] text-primary-foreground text-xs sm:text-sm">Ações</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{loading ? ( {loading ? (
<TableRow> <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 Carregando
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -687,7 +690,7 @@ export default function DoutoresPage() {
)) ))
) : ( ) : (
<TableRow> <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 Nenhum médico encontrado
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -696,64 +699,126 @@ export default function DoutoresPage() {
</Table> </Table>
</div> </div>
{/* Controles de paginação */} {/* Cards para mobile (md: hidden) */}
<div className="flex items-center justify-between"> <div className="md:hidden space-y-2">
<div className="flex items-center gap-2"> {loading ? (
<span className="text-sm text-muted-foreground">Itens por página:</span> <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 <select
value={itemsPerPage} value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))} 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={10}>10</option>
<option value={15}>15</option> <option value={15}>15</option>
<option value={20}>20</option> <option value={20}>20</option>
</select> </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{" "} Mostrando {paginatedDoctors.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
{Math.min(currentPage * itemsPerPage, displayedDoctors.length)} de {displayedDoctors.length} {Math.min(currentPage * itemsPerPage, displayedDoctors.length)} de {displayedDoctors.length}
</span> </span>
</div> </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 <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage(1)} onClick={() => setCurrentPage(1)}
disabled={currentPage === 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>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))} onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 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> </Button>
<span className="text-sm text-muted-foreground"> <span className="text-muted-foreground text-xs sm:text-sm">
Página {currentPage} de {totalPages || 1} Pág {currentPage} de {totalPages || 1}
</span> </span>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))} onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0} 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>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage(totalPages)} onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0} 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> </Button>
</div> </div>
</div> </div>

View File

@ -256,40 +256,53 @@ export default function PacientesPage() {
} }
return ( 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="flex items-center justify-between gap-4 flex-wrap"> {/* Header responsivo */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3 sm:gap-4">
<div> <div>
<h1 className="text-2xl font-bold">Pacientes</h1> <h1 className="text-2xl sm:text-3xl font-bold">Pacientes</h1>
<p className="text-muted-foreground">Gerencie os pacientes</p> <p className="text-xs sm:text-sm text-muted-foreground">Gerencie os pacientes</p>
</div> </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"> {/* Filtros e busca responsivos */}
{/* Busca */} <div className="space-y-2 sm:space-y-3">
<div className="relative"> {/* 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" /> <Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input <Input
className="pl-8 w-80" className="pl-8 w-full text-xs sm:text-sm h-8 sm:h-9"
placeholder="Buscar por nome, CPF ou ID…" placeholder="Nome, CPF ou ID…"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()} onKeyDown={(e) => e.key === "Enter" && handleBuscarServidor()}
/> />
</div> </div>
<Button variant="secondary" onClick={() => void handleBuscarServidor()} className="hover:bg-primary hover:text-white"> <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">
Buscar <span className="hidden sm:inline">Buscar</span>
<span className="sm:hidden">Ir</span>
</Button> </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 */} {/* Ordenar por */}
<select <select
aria-label="Ordenar por" aria-label="Ordenar por"
value={sortBy} value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)} 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_asc">AZ</option>
<option value="name_desc">Nome (ZA)</option> <option value="name_desc">ZA</option>
<option value="recent">Mais recentes (carregamento)</option> <option value="recent">Recentes</option>
<option value="oldest">Mais antigos (carregamento)</option> <option value="oldest">Antigos</option>
</select> </select>
{/* Estado (UF) */} {/* Estado (UF) */}
@ -300,9 +313,9 @@ export default function PacientesPage() {
setStateFilter(e.target.value); setStateFilter(e.target.value);
setCityFilter(""); 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) => ( {stateOptions.map((uf) => (
<option key={uf} value={uf}>{uf}</option> <option key={uf} value={uf}>{uf}</option>
))} ))}
@ -313,42 +326,38 @@ export default function PacientesPage() {
aria-label="Filtrar por cidade" aria-label="Filtrar por cidade"
value={cityFilter} value={cityFilter}
onChange={(e) => setCityFilter(e.target.value)} 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) => ( {cityOptions.map((c) => (
<option key={c} value={c}>{c}</option> <option key={c} value={c}>{c}</option>
))} ))}
</select> </select>
<Button onClick={handleAdd}>
<Plus className="mr-2 h-4 w-4" />
Novo paciente
</Button>
</div> </div>
</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> <Table>
<TableHeader> <TableHeader>
<TableRow className="bg-primary hover:bg-primary"> <TableRow className="bg-primary hover:bg-primary">
<TableHead className="text-primary-foreground">Nome</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Nome</TableHead>
<TableHead className="text-primary-foreground">CPF</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">CPF</TableHead>
<TableHead className="text-primary-foreground">Telefone</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Telefone</TableHead>
<TableHead className="text-primary-foreground">Cidade</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Cidade</TableHead>
<TableHead className="text-primary-foreground">Estado</TableHead> <TableHead className="text-primary-foreground text-xs sm:text-sm">Estado</TableHead>
<TableHead className="w-[100px] text-primary-foreground">Ações</TableHead> <TableHead className="w-[100px] text-primary-foreground text-xs sm:text-sm">Ações</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{paginatedData.length > 0 ? ( {paginatedData.length > 0 ? (
paginatedData.map((p) => ( paginatedData.map((p) => (
<TableRow key={p.id}> <TableRow key={p.id}>
<TableCell className="font-medium">{p.full_name || "(sem nome)"}</TableCell> <TableCell className="font-medium text-xs sm:text-sm">{p.full_name || "(sem nome)"}</TableCell>
<TableCell>{p.cpf || "-"}</TableCell> <TableCell className="text-xs sm:text-sm">{p.cpf || "-"}</TableCell>
<TableCell>{p.phone_mobile || "-"}</TableCell> <TableCell className="text-xs sm:text-sm">{p.phone_mobile || "-"}</TableCell>
<TableCell>{p.city || "-"}</TableCell> <TableCell className="text-xs sm:text-sm">{p.city || "-"}</TableCell>
<TableCell>{p.state || "-"}</TableCell> <TableCell className="text-xs sm:text-sm">{p.state || "-"}</TableCell>
<TableCell> <TableCell>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -381,7 +390,7 @@ export default function PacientesPage() {
)) ))
) : ( ) : (
<TableRow> <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 Nenhum paciente encontrado
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -390,64 +399,132 @@ export default function PacientesPage() {
</Table> </Table>
</div> </div>
{/* Controles de paginação */} {/* Mobile Cards - Hidden on desktop */}
<div className="flex items-center justify-between"> <div className="md:hidden space-y-2">
<div className="flex items-center gap-2"> {paginatedData.length > 0 ? (
<span className="text-sm text-muted-foreground">Itens por página:</span> 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 <select
value={itemsPerPage} value={itemsPerPage}
onChange={(e) => setItemsPerPage(Number(e.target.value))} 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={10}>10</option>
<option value={15}>15</option> <option value={15}>15</option>
<option value={20}>20</option> <option value={20}>20</option>
</select> </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{" "} Mostrando {paginatedData.length > 0 ? (currentPage - 1) * itemsPerPage + 1 : 0} a{" "}
{Math.min(currentPage * itemsPerPage, filtered.length)} de {filtered.length} {Math.min(currentPage * itemsPerPage, filtered.length)} de {filtered.length}
</span> </span>
</div> </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 <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage(1)} onClick={() => setCurrentPage(1)}
disabled={currentPage === 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>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))} onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 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> </Button>
<span className="text-sm text-muted-foreground"> <span className="text-muted-foreground text-xs sm:text-sm">
Página {currentPage} de {totalPages || 1} Pág {currentPage} de {totalPages || 1}
</span> </span>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))} onClick={() => setCurrentPage((prev) => Math.min(totalPages, prev + 1))}
disabled={currentPage === totalPages || totalPages === 0} 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>
<Button <Button
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setCurrentPage(totalPages)} onClick={() => setCurrentPage(totalPages)}
disabled={currentPage === totalPages || totalPages === 0} 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> </Button>
</div> </div>
</div> </div>

View File

@ -866,11 +866,15 @@ const ProfissionalPage = () => {
{appointment.type} {appointment.type}
</div> </div>
</div> </div>
<div className="hidden md:flex items-center justify-end"> <div className="flex items-center justify-end">
<div className="relative group"> {/* Tornar o trigger focusable/touchable para mobile: tabIndex + classes responsivas */}
<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"> <div className="relative group" tabIndex={0} role="button" aria-label={`Informações da consulta ${appointment.title}`}>
Ver informações do paciente {/* Tooltip: em telas pequenas com scrollbar horizontal permanente (overflow-x-scroll); desktop sem scroll (hover) */}
<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="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> </div>
</div> </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"> <div className="absolute top-full left-0 mt-1 z-50 bg-card border border-border rounded-md shadow-lg p-3">
<CalendarComponent <CalendarComponent
mode="single" 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) => { onSelect={(date) => {
if (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 }); onFormChange({ ...formData, appointmentDate: dateStr });
setShowDatePicker(false); 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> </div>
)} )}

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { useEffect, useMemo, useState } from "react"; 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 { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; 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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; 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 { 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 { import {
criarMedico, criarMedico,
atualizarMedico, atualizarMedico,
@ -76,7 +78,7 @@ type FormData = {
cpf: string; cpf: string;
rg: string; rg: string;
sexo: string; sexo: string;
data_nascimento: string; data_nascimento: Date | null;
email: string; email: string;
telefone: string; telefone: string;
celular: string; celular: string;
@ -109,7 +111,7 @@ const initial: FormData = {
cpf: "", cpf: "",
rg: "", rg: "",
sexo: "", sexo: "",
data_nascimento: "", data_nascimento: null,
email: "", email: "",
telefone: "", telefone: "",
celular: "", // Aqui, 'celular' pode ser 'phone_mobile' celular: "", // Aqui, 'celular' pode ser 'phone_mobile'
@ -150,7 +152,7 @@ export function DoctorRegistrationForm({
}: DoctorRegistrationFormProps) { }: DoctorRegistrationFormProps) {
const [form, setForm] = useState<FormData>(initial); const [form, setForm] = useState<FormData>(initial);
const [errors, setErrors] = useState<Record<string, string>>({}); 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 [isSubmitting, setSubmitting] = useState(false);
const [isUploadingPhoto, setUploadingPhoto] = useState(false); const [isUploadingPhoto, setUploadingPhoto] = useState(false);
const [isSearchingCEP, setSearchingCEP] = useState(false); const [isSearchingCEP, setSearchingCEP] = useState(false);
@ -257,7 +259,7 @@ export function DoctorRegistrationForm({
cpf: String(m.cpf || ""), cpf: String(m.cpf || ""),
rg: String(m.rg || m.document_number || ""), rg: String(m.rg || m.document_number || ""),
sexo: normalizeSex(m.sexo || m.sex || m.sexualidade || null) ?? "", 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 || ""), email: String(m.email || ""),
telefone: String(m.telefone || m.phone_mobile || m.phone || m.mobile || ""), telefone: String(m.telefone || m.phone_mobile || m.phone || m.mobile || ""),
celular: String(m.celular || m.phone2 || ""), celular: String(m.celular || m.phone2 || ""),
@ -430,36 +432,6 @@ function setField<T extends keyof FormData>(k: T, v: FormData[T]) {
} }
function toPayload(): MedicoInput { 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 { return {
user_id: null, user_id: null,
crm: form.crm || "", crm: form.crm || "",
@ -477,7 +449,7 @@ function toPayload(): MedicoInput {
neighborhood: form.bairro || undefined, neighborhood: form.bairro || undefined,
city: form.cidade || "", city: form.cidade || "",
state: form.estado || "", state: form.estado || "",
birth_date: isoDate, birth_date: form.data_nascimento ? form.data_nascimento.toISOString().slice(0, 10) : null,
rg: form.rg || null, rg: form.rg || null,
active: true, active: true,
created_by: null, created_by: null,
@ -796,7 +768,7 @@ async function handleSubmit(ev: React.FormEvent) {
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Sexo</Label> <Label>Sexo</Label>
<Select value={form.sexo} onValueChange={(v) => setField("sexo", v)}> <Select value={form.sexo} onValueChange={(v) => setField("sexo", v)}>
@ -811,23 +783,29 @@ async function handleSubmit(ev: React.FormEvent) {
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Data de Nascimento</Label> <Label htmlFor="data_nascimento_input">Data de Nascimento</Label>
<Input <Popover>
placeholder="dd/mm/aaaa" <PopoverTrigger asChild>
value={form.data_nascimento} <Button
onChange={(e) => { variant={"outline"}
const v = e.target.value.replace(/[^0-9\/]/g, "").slice(0, 10); className={cn(
setField("data_nascimento", v); "w-full justify-start text-left font-normal",
}} !form.data_nascimento && "text-muted-foreground"
onBlur={() => { )}
const raw = form.data_nascimento; >
const parts = raw.split(/\D+/).filter(Boolean); <CalendarIcon className="mr-2 h-4 w-4" />
if (parts.length === 3) { {form.data_nascimento ? format(form.data_nascimento, "dd/MM/yyyy") : <span>Selecione uma data</span>}
const d = `${parts[0].padStart(2,'0')}/${parts[1].padStart(2,'0')}/${parts[2].padStart(4,'0')}`; </Button>
setField("data_nascimento", d); </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>
</div> </div>
</CardContent> </CardContent>
@ -949,98 +927,6 @@ async function handleSubmit(ev: React.FormEvent) {
</Card> </Card>
</Collapsible> </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 }))}> <Collapsible open={expanded.endereco} onOpenChange={() => setExpanded((s) => ({ ...s, endereco: !s.endereco }))}>
<Card> <Card>
<CollapsibleTrigger asChild> <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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
import { Alert, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertDescription } from "@/components/ui/alert";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; 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 { import {
Paciente, Paciente,
@ -51,7 +54,7 @@ type FormData = {
cpf: string; cpf: string;
rg: string; rg: string;
sexo: string; sexo: string;
birth_date: string; birth_date: Date | null;
email: string; email: string;
telefone: string; telefone: string;
cep: string; cep: string;
@ -72,7 +75,7 @@ const initial: FormData = {
cpf: "", cpf: "",
rg: "", rg: "",
sexo: "", sexo: "",
birth_date: "", birth_date: null,
email: "", email: "",
telefone: "", telefone: "",
cep: "", cep: "",
@ -150,7 +153,7 @@ export function PatientRegistrationForm({
cpf: p.cpf || "", cpf: p.cpf || "",
rg: p.rg || "", rg: p.rg || "",
sexo: p.sex || "", 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 || "", telefone: p.phone_mobile || "",
email: p.email || "", email: p.email || "",
cep: p.cep || "", cep: p.cep || "",
@ -212,44 +215,13 @@ export function PatientRegistrationForm({
} }
function toPayload(): PacienteInput { 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 { return {
full_name: form.nome, full_name: form.nome,
social_name: form.nome_social || null, social_name: form.nome_social || null,
cpf: form.cpf, cpf: form.cpf,
rg: form.rg || null, rg: form.rg || null,
sex: form.sexo || 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, phone_mobile: form.telefone || null,
email: form.email || null, email: form.email || null,
cep: form.cep || null, cep: form.cep || null,
@ -376,7 +348,7 @@ export function PatientRegistrationForm({
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="flex items-center gap-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"> <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>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="photo" className="cursor-pointer rounded-md transition-colors"> <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 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>
<div className="grid grid-cols-2 gap-4"> <div className="space-y-4">
<div className="space-y-2"><Label>Sexo</Label> <div className="space-y-2">
<Label>Sexo</Label>
<Select value={form.sexo} onValueChange={(v) => setField("sexo", v)}> <Select value={form.sexo} onValueChange={(v) => setField("sexo", v)}>
<SelectTrigger><SelectValue placeholder="Selecione o sexo" /></SelectTrigger> <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> </Select>
</div> </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> </div>
</CardContent> </CardContent>
</CollapsibleContent> </CollapsibleContent>

View File

@ -872,7 +872,8 @@ function MonthView({
} }
return ( 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"> <div className="grid grid-cols-7 border-b">
{["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb"].map((day) => ( {["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"> <div key={day} className="border-r p-2 text-center text-xs font-medium last:border-r-0 sm:text-sm">