Compare commits
4 Commits
171c954a78
...
4cec1582ce
| Author | SHA1 | Date | |
|---|---|---|---|
| 4cec1582ce | |||
| a0dfcd671c | |||
| 5858886efd | |||
| 62cced521f |
@ -9,7 +9,6 @@ import {
|
||||
getUpcomingAppointments,
|
||||
getAppointmentsByDateRange,
|
||||
getNewUsersLastDays,
|
||||
getPendingReports,
|
||||
getDisabledUsers,
|
||||
getDoctorsAvailabilityToday,
|
||||
getPatientById,
|
||||
@ -18,7 +17,7 @@ import {
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
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 { PatientRegistrationForm } from '@/components/features/forms/patient-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 [appointmentData, setAppointmentData] = useState<any[]>([]);
|
||||
const [newUsers, setNewUsers] = useState<any[]>([]);
|
||||
const [pendingReports, setPendingReports] = useState<any[]>([]);
|
||||
const [disabledUsers, setDisabledUsers] = useState<any[]>([]);
|
||||
const [doctors, setDoctors] = 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
|
||||
const [upcomingAppts, appointmentDataRange, newUsersList, pendingReportsList, disabledUsersList] = await Promise.all([
|
||||
const [upcomingAppts, appointmentDataRange, newUsersList, disabledUsersList] = await Promise.all([
|
||||
getUpcomingAppointments(5),
|
||||
getAppointmentsByDateRange(7),
|
||||
getNewUsersLastDays(7),
|
||||
getPendingReports(5),
|
||||
getDisabledUsers(5),
|
||||
]);
|
||||
|
||||
setAppointments(upcomingAppts);
|
||||
setAppointmentData(appointmentDataRange);
|
||||
setNewUsers(newUsersList);
|
||||
setPendingReports(pendingReportsList);
|
||||
setDisabledUsers(disabledUsersList);
|
||||
|
||||
// 3. Busca detalhes de pacientes e médicos para as próximas consultas
|
||||
@ -264,15 +260,7 @@ export default function DashboardPage() {
|
||||
</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>
|
||||
|
||||
{/* 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="sm:hidden">Médico</span>
|
||||
</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>
|
||||
|
||||
@ -330,28 +313,7 @@ export default function DashboardPage() {
|
||||
)}
|
||||
</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>
|
||||
|
||||
{/* 4. NOVOS USUÁRIOS */}
|
||||
|
||||
@ -2,10 +2,11 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { FileDown, BarChart2, Users, CalendarCheck } from "lucide-react";
|
||||
import jsPDF from "jspdf";
|
||||
import html2canvas from "html2canvas";
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
|
||||
import {
|
||||
countAppointmentsToday,
|
||||
@ -30,10 +31,51 @@ const FALLBACK_MEDICOS = [
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
function exportPDF(title: string, content: string) {
|
||||
async function exportPDF(title: string, content: string, chartElementId?: string) {
|
||||
const doc = new jsPDF();
|
||||
doc.text(title, 10, 10);
|
||||
doc.text(content, 10, 20);
|
||||
let yPosition = 15;
|
||||
|
||||
// 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`);
|
||||
}
|
||||
|
||||
@ -203,7 +245,7 @@ export default function RelatoriosPage() {
|
||||
size="sm"
|
||||
variant="outline"
|
||||
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
|
||||
</Button>
|
||||
@ -211,15 +253,17 @@ export default function RelatoriosPage() {
|
||||
{loading ? (
|
||||
<div className="h-[220px] flex items-center justify-center text-muted-foreground">Carregando dados...</div>
|
||||
) : (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<BarChart data={consultasData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="periodo" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="consultas" fill="#6366f1" name="Consultas" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
<div id="chart-consultas">
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<BarChart data={consultasData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="periodo" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Bar dataKey="consultas" fill="#6366f1" name="Consultas" />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</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="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>
|
||||
<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>
|
||||
<table className="w-full text-sm mt-4">
|
||||
<div id="table-pacientes">
|
||||
<table className="w-full text-sm mt-4">
|
||||
<thead>
|
||||
<tr className="text-muted-foreground">
|
||||
<th className="text-left font-medium">Paciente</th>
|
||||
@ -257,15 +302,17 @@ export default function RelatoriosPage() {
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Médicos mais produtivos */}
|
||||
<div className="bg-card border border-border rounded-lg shadow p-6">
|
||||
<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>
|
||||
<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>
|
||||
<table className="w-full text-sm mt-4">
|
||||
<div id="table-medicos">
|
||||
<table className="w-full text-sm mt-4">
|
||||
<thead>
|
||||
<tr className="text-muted-foreground">
|
||||
<th className="text-left font-medium">Médico</th>
|
||||
@ -291,6 +338,7 @@ export default function RelatoriosPage() {
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1888,31 +1888,20 @@ export default function PacientePage() {
|
||||
<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>
|
||||
|
||||
{isEditingProfile ? (
|
||||
<div className="flex justify-center">
|
||||
<UploadAvatar
|
||||
userId={profileData.id}
|
||||
currentAvatarUrl={profileData.foto_url || "/avatars/01.png"}
|
||||
onAvatarChange={(newUrl) => handleProfileChange('foto_url', newUrl)}
|
||||
userName={profileData.nome}
|
||||
/>
|
||||
</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="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">
|
||||
<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'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-center space-y-2">
|
||||
<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'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -384,37 +384,37 @@ export function EventManager({
|
||||
{/* Desktop: Button group */}
|
||||
<div className="hidden sm:flex items-center gap-1 rounded-lg border bg-background p-1">
|
||||
<Button
|
||||
variant={view === "month" ? "secondary" : "ghost"}
|
||||
variant={view === "month" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
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" />
|
||||
<span className="ml-1">Mês</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={view === "week" ? "secondary" : "ghost"}
|
||||
variant={view === "week" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
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" />
|
||||
<span className="ml-1">Semana</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={view === "day" ? "secondary" : "ghost"}
|
||||
variant={view === "day" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
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" />
|
||||
<span className="ml-1">Dia</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={view === "list" ? "secondary" : "ghost"}
|
||||
variant={view === "list" ? "default" : "ghost"}
|
||||
size="sm"
|
||||
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" />
|
||||
<span className="ml-1">Lista</span>
|
||||
@ -432,7 +432,7 @@ export function EventManager({
|
||||
aria-label="Buscar"
|
||||
className="flex items-center justify-center h-10 w-10 p-0 text-muted-foreground bg-transparent border-0"
|
||||
onClick={() => {
|
||||
const el = document.querySelector<HTMLInputElement>('input[placeholder="Buscar eventos..."]')
|
||||
const el = document.querySelector<HTMLInputElement>('input[placeholder="Buscar pacientes..."]')
|
||||
el?.focus()
|
||||
}}
|
||||
>
|
||||
@ -441,7 +441,7 @@ export function EventManager({
|
||||
|
||||
{/* Input central com altura consistente e foco visível */}
|
||||
<Input
|
||||
placeholder="Buscar eventos..."
|
||||
placeholder="Buscar paciente..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className={cn(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user