Merge branch 'feature/admin-improve-responsiveness' into backup/reports
This commit is contained in:
commit
6b3dc6255f
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 (A–Z)</option>
|
||||
<option value="name_desc">Nome (Z–A)</option>
|
||||
<option value="recent">Mais recentes (carregamento)</option>
|
||||
<option value="oldest">Mais antigos (carregamento)</option>
|
||||
<option value="name_asc">Nome A–Z</option>
|
||||
<option value="name_desc">Nome Z–A</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>
|
||||
|
||||
@ -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 (A–Z)</option>
|
||||
<option value="name_desc">Nome (Z–A)</option>
|
||||
<option value="recent">Mais recentes (carregamento)</option>
|
||||
<option value="oldest">Mais antigos (carregamento)</option>
|
||||
<option value="name_asc">A–Z</option>
|
||||
<option value="name_desc">Z–A</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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user