forked from RiseUP/riseup-squad20
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 (
|
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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 (A–Z)</option>
|
<option value="name_asc">Nome A–Z</option>
|
||||||
<option value="name_desc">Nome (Z–A)</option>
|
<option value="name_desc">Nome Z–A</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>
|
||||||
|
|||||||
@ -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 (A–Z)</option>
|
<option value="name_asc">A–Z</option>
|
||||||
<option value="name_desc">Nome (Z–A)</option>
|
<option value="name_desc">Z–A</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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user