Compare commits

...

4 Commits

4 changed files with 92 additions and 93 deletions

View File

@ -9,7 +9,6 @@ import {
getUpcomingAppointments, getUpcomingAppointments,
getAppointmentsByDateRange, getAppointmentsByDateRange,
getNewUsersLastDays, getNewUsersLastDays,
getPendingReports,
getDisabledUsers, getDisabledUsers,
getDoctorsAvailabilityToday, getDoctorsAvailabilityToday,
getPatientById, getPatientById,
@ -18,7 +17,7 @@ import {
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Alert, AlertDescription } from '@/components/ui/alert'; import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle, Calendar, Users, Stethoscope, Clock, FileText, AlertTriangle, Plus, ArrowLeft } from 'lucide-react'; import { AlertCircle, Calendar, Users, Stethoscope, Clock, AlertTriangle, Plus, ArrowLeft } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { PatientRegistrationForm } from '@/components/features/forms/patient-registration-form'; import { PatientRegistrationForm } from '@/components/features/forms/patient-registration-form';
import { DoctorRegistrationForm } from '@/components/features/forms/doctor-registration-form'; import { DoctorRegistrationForm } from '@/components/features/forms/doctor-registration-form';
@ -49,7 +48,6 @@ export default function DashboardPage() {
const [appointments, setAppointments] = useState<UpcomingAppointment[]>([]); const [appointments, setAppointments] = useState<UpcomingAppointment[]>([]);
const [appointmentData, setAppointmentData] = useState<any[]>([]); const [appointmentData, setAppointmentData] = useState<any[]>([]);
const [newUsers, setNewUsers] = useState<any[]>([]); const [newUsers, setNewUsers] = useState<any[]>([]);
const [pendingReports, setPendingReports] = useState<any[]>([]);
const [disabledUsers, setDisabledUsers] = useState<any[]>([]); const [disabledUsers, setDisabledUsers] = useState<any[]>([]);
const [doctors, setDoctors] = useState<Map<string, any>>(new Map()); const [doctors, setDoctors] = useState<Map<string, any>>(new Map());
const [patients, setPatients] = useState<Map<string, any>>(new Map()); const [patients, setPatients] = useState<Map<string, any>>(new Map());
@ -83,18 +81,16 @@ export default function DashboardPage() {
}); });
// 2. Carrega dados dos widgets em paralelo // 2. Carrega dados dos widgets em paralelo
const [upcomingAppts, appointmentDataRange, newUsersList, pendingReportsList, disabledUsersList] = await Promise.all([ const [upcomingAppts, appointmentDataRange, newUsersList, disabledUsersList] = await Promise.all([
getUpcomingAppointments(5), getUpcomingAppointments(5),
getAppointmentsByDateRange(7), getAppointmentsByDateRange(7),
getNewUsersLastDays(7), getNewUsersLastDays(7),
getPendingReports(5),
getDisabledUsers(5), getDisabledUsers(5),
]); ]);
setAppointments(upcomingAppts); setAppointments(upcomingAppts);
setAppointmentData(appointmentDataRange); setAppointmentData(appointmentDataRange);
setNewUsers(newUsersList); setNewUsers(newUsersList);
setPendingReports(pendingReportsList);
setDisabledUsers(disabledUsersList); setDisabledUsers(disabledUsersList);
// 3. Busca detalhes de pacientes e médicos para as próximas consultas // 3. Busca detalhes de pacientes e médicos para as próximas consultas
@ -264,15 +260,7 @@ export default function DashboardPage() {
</div> </div>
</div> </div>
<div className="bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-border hover:shadow-md transition">
<div className="flex items-center justify-between gap-3">
<div className="min-w-0 flex-1">
<h3 className="text-xs sm:text-sm font-medium text-muted-foreground truncate">Relatórios Pendentes</h3>
<p className="text-2xl sm:text-3xl font-bold text-foreground mt-1 sm:mt-2">{pendingReports.length}</p>
</div>
<FileText className="h-6 sm:h-8 w-6 sm:w-8 text-orange-500 opacity-20 flex-shrink-0" />
</div>
</div>
</div> </div>
{/* 6. AÇÕES RÁPIDAS - Responsivo: stack em mobile, wrap em desktop */} {/* 6. AÇÕES RÁPIDAS - Responsivo: stack em mobile, wrap em desktop */}
@ -294,11 +282,6 @@ export default function DashboardPage() {
<span className="hidden sm:inline">Novo Médico</span> <span className="hidden sm:inline">Novo Médico</span>
<span className="sm:hidden">Médico</span> <span className="sm:hidden">Médico</span>
</Button> </Button>
<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" />
<span className="hidden sm:inline">Ver Relatórios</span>
<span className="sm:hidden">Relatórios</span>
</Button>
</div> </div>
</div> </div>
@ -330,28 +313,7 @@ export default function DashboardPage() {
)} )}
</div> </div>
{/* 5. RELATÓRIOS PENDENTES */}
<div className="bg-card p-4 sm:p-5 md:p-6 rounded-lg border border-border">
<h2 className="text-base sm:text-lg font-semibold text-foreground mb-3 sm:mb-4 flex items-center gap-2">
<FileText className="h-4 sm:h-5 w-4 sm:w-5" />
<span className="truncate">Pendentes</span>
</h2>
{pendingReports.length > 0 ? (
<div className="space-y-2">
{pendingReports.map(report => (
<div key={report.id} className="p-2 sm:p-3 bg-muted rounded-lg hover:bg-muted/80 transition cursor-pointer text-xs sm:text-sm">
<p className="font-medium text-foreground truncate">{report.order_number}</p>
<p className="text-[10px] sm:text-xs text-muted-foreground truncate">{report.exam || 'Sem descrição'}</p>
</div>
))}
<Button onClick={() => router.push('/dashboard/relatorios')} variant="ghost" className="w-full mt-2 hover:bg-primary! hover:text-white! transition-colors text-xs sm:text-sm" size="sm">
Ver Todos
</Button>
</div>
) : (
<p className="text-xs sm:text-sm text-muted-foreground">Sem relatórios pendentes</p>
)}
</div>
</div> </div>
{/* 4. NOVOS USUÁRIOS */} {/* 4. NOVOS USUÁRIOS */}

View File

@ -2,10 +2,11 @@
"use client"; "use client";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState, useRef } from "react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { FileDown, BarChart2, Users, CalendarCheck } from "lucide-react"; import { FileDown, BarChart2, Users, CalendarCheck } from "lucide-react";
import jsPDF from "jspdf"; import jsPDF from "jspdf";
import html2canvas from "html2canvas";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts"; import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
import { import {
countAppointmentsToday, countAppointmentsToday,
@ -30,10 +31,51 @@ const FALLBACK_MEDICOS = [
// Helper Functions // Helper Functions
// ============================================================================ // ============================================================================
function exportPDF(title: string, content: string) { async function exportPDF(title: string, content: string, chartElementId?: string) {
const doc = new jsPDF(); const doc = new jsPDF();
doc.text(title, 10, 10); let yPosition = 15;
doc.text(content, 10, 20);
// Add title
doc.setFontSize(16);
doc.setFont(undefined, "bold");
doc.text(title, 15, yPosition);
yPosition += 10;
// Add description/content
doc.setFontSize(11);
doc.setFont(undefined, "normal");
const contentLines = doc.splitTextToSize(content, 180);
doc.text(contentLines, 15, yPosition);
yPosition += contentLines.length * 5 + 15;
// Capture chart if chartElementId is provided
if (chartElementId) {
try {
const chartElement = document.getElementById(chartElementId);
if (chartElement) {
// Create a canvas from the chart element
const canvas = await html2canvas(chartElement, {
backgroundColor: "#ffffff",
scale: 2,
logging: false,
});
// Convert canvas to image
const imgData = canvas.toDataURL("image/png");
const imgWidth = 180;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// Add image to PDF
doc.addImage(imgData, "PNG", 15, yPosition, imgWidth, imgHeight);
yPosition += imgHeight + 10;
}
} catch (error) {
console.error("Error capturing chart:", error);
doc.text("(Erro ao capturar gráfico)", 15, yPosition);
yPosition += 10;
}
}
doc.save(`${title.toLowerCase().replace(/ /g, "-")}.pdf`); doc.save(`${title.toLowerCase().replace(/ /g, "-")}.pdf`);
} }
@ -203,7 +245,7 @@ export default function RelatoriosPage() {
size="sm" size="sm"
variant="outline" variant="outline"
className="hover:bg-primary! hover:text-white! transition-colors w-full md:w-auto" className="hover:bg-primary! hover:text-white! transition-colors w-full md:w-auto"
onClick={() => exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.")} onClick={() => exportPDF("Consultas por Período", "Resumo das consultas realizadas por período.", "chart-consultas")}
> >
<FileDown className="w-4 h-4 mr-1" /> Exportar PDF <FileDown className="w-4 h-4 mr-1" /> Exportar PDF
</Button> </Button>
@ -211,15 +253,17 @@ export default function RelatoriosPage() {
{loading ? ( {loading ? (
<div className="h-[220px] flex items-center justify-center text-muted-foreground">Carregando dados...</div> <div className="h-[220px] flex items-center justify-center text-muted-foreground">Carregando dados...</div>
) : ( ) : (
<ResponsiveContainer width="100%" height={220}> <div id="chart-consultas">
<BarChart data={consultasData}> <ResponsiveContainer width="100%" height={220}>
<CartesianGrid strokeDasharray="3 3" /> <BarChart data={consultasData}>
<XAxis dataKey="periodo" /> <CartesianGrid strokeDasharray="3 3" />
<YAxis /> <XAxis dataKey="periodo" />
<Tooltip /> <YAxis />
<Bar dataKey="consultas" fill="#6366f1" name="Consultas" /> <Tooltip />
</BarChart> <Bar dataKey="consultas" fill="#6366f1" name="Consultas" />
</ResponsiveContainer> </BarChart>
</ResponsiveContainer>
</div>
)} )}
</div> </div>
</div> </div>
@ -229,9 +273,10 @@ export default function RelatoriosPage() {
<div className="bg-card border border-border rounded-lg shadow p-6"> <div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Users className="w-5 h-5" /> Pacientes Mais Atendidos</h2> <h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Users className="w-5 h-5" /> Pacientes Mais Atendidos</h2>
<Button size="sm" variant="outline" className="hover:bg-primary! hover:text-white! transition-colors" onClick={() => exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button> <Button size="sm" variant="outline" className="hover:bg-primary! hover:text-white! transition-colors" onClick={() => exportPDF("Pacientes Mais Atendidos", "Lista dos pacientes mais atendidos.", "table-pacientes")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div> </div>
<table className="w-full text-sm mt-4"> <div id="table-pacientes">
<table className="w-full text-sm mt-4">
<thead> <thead>
<tr className="text-muted-foreground"> <tr className="text-muted-foreground">
<th className="text-left font-medium">Paciente</th> <th className="text-left font-medium">Paciente</th>
@ -257,15 +302,17 @@ export default function RelatoriosPage() {
)} )}
</tbody> </tbody>
</table> </table>
</div>
</div> </div>
{/* Médicos mais produtivos */} {/* Médicos mais produtivos */}
<div className="bg-card border border-border rounded-lg shadow p-6"> <div className="bg-card border border-border rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Users className="w-5 h-5" /> Médicos Mais Produtivos</h2> <h2 className="font-semibold text-lg text-foreground flex items-center gap-2"><Users className="w-5 h-5" /> Médicos Mais Produtivos</h2>
<Button size="sm" variant="outline" className="hover:bg-primary! hover:text-white! transition-colors" onClick={() => exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button> <Button size="sm" variant="outline" className="hover:bg-primary! hover:text-white! transition-colors" onClick={() => exportPDF("Médicos Mais Produtivos", "Lista dos médicos mais produtivos.", "table-medicos")}> <FileDown className="w-4 h-4 mr-1" /> Exportar PDF</Button>
</div> </div>
<table className="w-full text-sm mt-4"> <div id="table-medicos">
<table className="w-full text-sm mt-4">
<thead> <thead>
<tr className="text-muted-foreground"> <tr className="text-muted-foreground">
<th className="text-left font-medium">Médico</th> <th className="text-left font-medium">Médico</th>
@ -291,6 +338,7 @@ export default function RelatoriosPage() {
)} )}
</tbody> </tbody>
</table> </table>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1888,31 +1888,20 @@ export default function PacientePage() {
<div className="border border-border rounded-lg p-3 sm:p-4 md:p-6"> <div className="border border-border rounded-lg p-3 sm:p-4 md:p-6">
<h3 className="text-base sm:text-lg md:text-lg font-semibold mb-3 sm:mb-4">Foto do Perfil</h3> <h3 className="text-base sm:text-lg md:text-lg font-semibold mb-3 sm:mb-4">Foto do Perfil</h3>
{isEditingProfile ? ( <div className="flex flex-col items-center gap-3 sm:gap-4">
<div className="flex justify-center"> <Avatar className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28">
<UploadAvatar <AvatarImage src={profileData.foto_url} alt={profileData.nome || 'Avatar'} />
userId={profileData.id} <AvatarFallback className="bg-primary text-primary-foreground text-lg sm:text-xl md:text-2xl font-bold">
currentAvatarUrl={profileData.foto_url || "/avatars/01.png"} {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'}
onAvatarChange={(newUrl) => handleProfileChange('foto_url', newUrl)} </AvatarFallback>
userName={profileData.nome} </Avatar>
/>
</div>
) : (
<div className="flex flex-col items-center gap-3 sm:gap-4">
<Avatar className="h-20 w-20 sm:h-24 sm:w-24 md:h-28 md:w-28">
<AvatarImage src={profileData.foto_url} alt={profileData.nome || 'Avatar'} />
<AvatarFallback className="bg-primary text-primary-foreground text-lg sm:text-xl md:text-2xl font-bold">
{profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'}
</AvatarFallback>
</Avatar>
<div className="text-center space-y-2"> <div className="text-center space-y-2">
<p className="text-xs sm:text-sm md:text-base text-muted-foreground"> <p className="text-xs sm:text-sm md:text-base text-muted-foreground">
{profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'} {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'}
</p> </p>
</div>
</div> </div>
)} </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -384,37 +384,37 @@ export function EventManager({
{/* Desktop: Button group */} {/* Desktop: Button group */}
<div className="hidden sm:flex items-center gap-1 rounded-lg border bg-background p-1"> <div className="hidden sm:flex items-center gap-1 rounded-lg border bg-background p-1">
<Button <Button
variant={view === "month" ? "secondary" : "ghost"} variant={view === "month" ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => setView("month")} onClick={() => setView("month")}
className="h-8" className={cn("h-8", view !== "month" && "hover:bg-blue-500 hover:text-white dark:hover:bg-blue-600 dark:hover:text-white")}
> >
<Calendar className="h-4 w-4" /> <Calendar className="h-4 w-4" />
<span className="ml-1">Mês</span> <span className="ml-1">Mês</span>
</Button> </Button>
<Button <Button
variant={view === "week" ? "secondary" : "ghost"} variant={view === "week" ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => setView("week")} onClick={() => setView("week")}
className="h-8" className={cn("h-8", view !== "week" && "hover:bg-blue-500 hover:text-white dark:hover:bg-blue-600 dark:hover:text-white")}
> >
<Grid3x3 className="h-4 w-4" /> <Grid3x3 className="h-4 w-4" />
<span className="ml-1">Semana</span> <span className="ml-1">Semana</span>
</Button> </Button>
<Button <Button
variant={view === "day" ? "secondary" : "ghost"} variant={view === "day" ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => setView("day")} onClick={() => setView("day")}
className="h-8" className={cn("h-8", view !== "day" && "hover:bg-blue-500 hover:text-white dark:hover:bg-blue-600 dark:hover:text-white")}
> >
<Clock className="h-4 w-4" /> <Clock className="h-4 w-4" />
<span className="ml-1">Dia</span> <span className="ml-1">Dia</span>
</Button> </Button>
<Button <Button
variant={view === "list" ? "secondary" : "ghost"} variant={view === "list" ? "default" : "ghost"}
size="sm" size="sm"
onClick={() => setView("list")} onClick={() => setView("list")}
className="h-8" className={cn("h-8", view !== "list" && "hover:bg-blue-500 hover:text-white dark:hover:bg-blue-600 dark:hover:text-white")}
> >
<List className="h-4 w-4" /> <List className="h-4 w-4" />
<span className="ml-1">Lista</span> <span className="ml-1">Lista</span>
@ -432,7 +432,7 @@ export function EventManager({
aria-label="Buscar" aria-label="Buscar"
className="flex items-center justify-center h-10 w-10 p-0 text-muted-foreground bg-transparent border-0" className="flex items-center justify-center h-10 w-10 p-0 text-muted-foreground bg-transparent border-0"
onClick={() => { onClick={() => {
const el = document.querySelector<HTMLInputElement>('input[placeholder="Buscar eventos..."]') const el = document.querySelector<HTMLInputElement>('input[placeholder="Buscar pacientes..."]')
el?.focus() el?.focus()
}} }}
> >
@ -441,7 +441,7 @@ export function EventManager({
{/* Input central com altura consistente e foco visível */} {/* Input central com altura consistente e foco visível */}
<Input <Input
placeholder="Buscar eventos..." placeholder="Buscar paciente..."
value={searchQuery} value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)} onChange={(e) => setSearchQuery(e.target.value)}
className={cn( className={cn(