commit
248e90595e
@ -2,12 +2,7 @@
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Eye, Edit, Calendar, Trash2, Loader2, MoreVertical } from "lucide-react";
|
||||
import { api } from "@/services/api.mjs";
|
||||
import { PatientDetailsModal } from "@/components/ui/patient-details-modal";
|
||||
@ -202,15 +197,11 @@ export default function PacientesPage() {
|
||||
|
||||
<div className="bg-card rounded-lg border border-border overflow-hidden shadow-md">
|
||||
{/* Tabela para Telas Médias e Grandes */}
|
||||
<div className="overflow-x-auto hidden md:block">
|
||||
{" "}
|
||||
{/* Esconde em telas pequenas */}
|
||||
<div className="overflow-x-auto hidden md:block"> {/* Esconde em telas pequenas */}
|
||||
<table className="min-w-[600px] w-full">
|
||||
<thead className="bg-muted border-b border-border">
|
||||
<tr>
|
||||
<th className="text-left p-3 sm:p-4 font-medium text-foreground">
|
||||
Nome
|
||||
</th>
|
||||
<th className="text-left p-3 sm:p-4 font-medium text-foreground">Nome</th>
|
||||
<th className="text-left p-3 sm:p-4 font-medium text-foreground">
|
||||
Telefone
|
||||
</th>
|
||||
@ -226,35 +217,24 @@ export default function PacientesPage() {
|
||||
<th className="text-left p-3 sm:p-4 font-medium text-foreground hidden xl:table-cell">
|
||||
Próximo atendimento
|
||||
</th>
|
||||
<th className="text-left p-3 sm:p-4 font-medium text-foreground">
|
||||
Ações
|
||||
</th>
|
||||
<th className="text-left p-3 sm:p-4 font-medium text-foreground">Ações</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
className="p-6 text-muted-foreground text-center"
|
||||
>
|
||||
<td colSpan={7} className="p-6 text-muted-foreground text-center">
|
||||
<Loader2 className="w-6 h-6 animate-spin mx-auto text-primary" />
|
||||
Carregando pacientes...
|
||||
</td>
|
||||
</tr>
|
||||
) : error ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
className="p-6 text-red-600 text-center"
|
||||
>{`Erro: ${error}`}</td>
|
||||
<td colSpan={7} className="p-6 text-red-600 text-center">{`Erro: ${error}`}</td>
|
||||
</tr>
|
||||
) : pacientes.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={7}
|
||||
className="p-8 text-center text-muted-foreground"
|
||||
>
|
||||
<td colSpan={7} className="p-8 text-center text-muted-foreground">
|
||||
Nenhum paciente encontrado
|
||||
</td>
|
||||
</tr>
|
||||
@ -289,9 +269,7 @@ export default function PacientesPage() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleOpenModal(p)}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => handleOpenModal(p)}>
|
||||
<Eye className="w-4 h-4 mr-2" />
|
||||
Ver detalhes
|
||||
</DropdownMenuItem>
|
||||
@ -301,11 +279,13 @@ export default function PacientesPage() {
|
||||
Laudos
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
{/* <DropdownMenuItem onClick={() => alert(`Agenda para paciente ID: ${p.id}`)}>
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
Ver agenda
|
||||
</DropdownMenuItem> */}
|
||||
{/* <DropdownMenuItem
|
||||
onClick={() => {
|
||||
const newPacientes = pacientes.filter(
|
||||
(pac) => pac.id !== p.id
|
||||
);
|
||||
const newPacientes = pacientes.filter((pac) => pac.id !== p.id);
|
||||
setPacientes(newPacientes);
|
||||
alert(`Paciente ID: ${p.id} excluído`);
|
||||
}}
|
||||
@ -313,7 +293,7 @@ export default function PacientesPage() {
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Excluir
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItem> */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</td>
|
||||
@ -325,9 +305,7 @@ export default function PacientesPage() {
|
||||
</div>
|
||||
|
||||
{/* Layout em Cards/Lista para Telas Pequenas */}
|
||||
<div className="md:hidden divide-y divide-border">
|
||||
{" "}
|
||||
{/* Visível apenas em telas pequenas */}
|
||||
<div className="md:hidden divide-y divide-border"> {/* Visível apenas em telas pequenas */}
|
||||
{loading ? (
|
||||
<div className="p-6 text-muted-foreground text-center">
|
||||
<Loader2 className="w-6 h-6 animate-spin mx-auto text-primary" />
|
||||
@ -341,16 +319,9 @@ export default function PacientesPage() {
|
||||
</div>
|
||||
) : (
|
||||
currentItems.map((p) => (
|
||||
<div
|
||||
key={p.id}
|
||||
className="flex items-center justify-between p-4 hover:bg-accent/40 transition-colors"
|
||||
>
|
||||
<div className="flex-1 min-w-0 pr-4">
|
||||
{" "}
|
||||
{/* Adicionado padding à direita */}
|
||||
<div className="text-base font-semibold text-foreground break-words">
|
||||
{" "}
|
||||
{/* Aumentado a fonte e break-words para evitar corte do nome */}
|
||||
<div key={p.id} className="flex items-center justify-between p-4 hover:bg-accent/40 transition-colors">
|
||||
<div className="flex-1 min-w-0 pr-4"> {/* Adicionado padding à direita */}
|
||||
<div className="text-base font-semibold text-foreground break-words"> {/* Aumentado a fonte e break-words para evitar corte do nome */}
|
||||
{p.nome || "—"}
|
||||
</div>
|
||||
{/* Removido o 'truncate' e adicionado 'break-words' no telefone */}
|
||||
@ -376,19 +347,13 @@ export default function PacientesPage() {
|
||||
Laudos
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
alert(`Agenda para paciente ID: ${p.id}`)
|
||||
}
|
||||
>
|
||||
<DropdownMenuItem onClick={() => alert(`Agenda para paciente ID: ${p.id}`)}>
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
Ver agenda
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
const newPacientes = pacientes.filter(
|
||||
(pac) => pac.id !== p.id
|
||||
);
|
||||
const newPacientes = pacientes.filter((pac) => pac.id !== p.id);
|
||||
setPacientes(newPacientes);
|
||||
alert(`Paciente ID: ${p.id} excluído`);
|
||||
}}
|
||||
@ -405,6 +370,7 @@ export default function PacientesPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
{/* Paginação */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex flex-wrap justify-center items-center gap-2 border-t border-border p-4 bg-muted/40">
|
||||
|
||||
@ -3,14 +3,13 @@
|
||||
import React, { useEffect, useState, useCallback, useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Edit, Trash2, Eye, Calendar, Loader2, MoreVertical } from "lucide-react";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Edit, Trash2, Eye, Calendar, Filter, Loader2, MoreVertical } from "lucide-react"
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"
|
||||
|
||||
// Imports dos Serviços
|
||||
import { doctorsService } from "@/services/doctorsApi.mjs";
|
||||
import { doctorsService } from "services/doctorsApi.mjs";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
// --- NOVOS IMPORTS (Certifique-se que criou os arquivos no passo anterior) ---
|
||||
@ -326,9 +325,10 @@ export default function DoctorsPage() {
|
||||
<td className="px-4 py-3 text-right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="text-black-600 cursor-pointer inline-block">
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Abrir menu</span>
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</div>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => openDetailsDialog(doctor)}>
|
||||
|
||||
@ -3,39 +3,10 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Eye,
|
||||
Calendar,
|
||||
Filter,
|
||||
Loader2,
|
||||
MoreVertical,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Edit, Trash2, Eye, Calendar, Filter, Loader2, MoreVertical } from "lucide-react";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||
import { patientsService } from "@/services/patientsApi.mjs";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
@ -75,7 +46,8 @@ export default function PacientesPage() {
|
||||
// --- FUNÇÕES DE LÓGICA ---
|
||||
|
||||
// 1. Função para carregar TODOS os pacientes da API
|
||||
const fetchAllPacientes = useCallback(async () => {
|
||||
const fetchAllPacientes = useCallback(
|
||||
async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
@ -89,10 +61,8 @@ export default function PacientesPage() {
|
||||
cidade: p.city ?? "—",
|
||||
estado: p.state ?? "—",
|
||||
// Formate as datas se necessário, aqui usamos como string
|
||||
ultimoAtendimento: p.last_visit_at?.split("T")[0] ?? "—",
|
||||
proximoAtendimento: p.next_appointment_at
|
||||
? p.next_appointment_at.split("T")[0].split("-").reverse().join("-")
|
||||
: "—",
|
||||
ultimoAtendimento: p.last_visit_at?.split('T')[0] ?? "—",
|
||||
proximoAtendimento: p.next_appointment_at?.split('T')[0] ?? "—",
|
||||
vip: Boolean(p.vip ?? false),
|
||||
convenio: p.convenio ?? "Particular", // Define um valor padrão
|
||||
status: p.status ?? undefined,
|
||||
@ -117,7 +87,8 @@ export default function PacientesPage() {
|
||||
|
||||
// Filtro por Convênio
|
||||
const matchesConvenio =
|
||||
convenioFilter === "all" || patient.convenio === convenioFilter;
|
||||
convenioFilter === "all" ||
|
||||
patient.convenio === convenioFilter;
|
||||
|
||||
// Filtro por VIP
|
||||
const matchesVip =
|
||||
@ -224,13 +195,9 @@ export default function PacientesPage() {
|
||||
|
||||
{/* VIP - Ocupa a largura total em telas pequenas, depois se ajusta */}
|
||||
<div className="flex items-center gap-2 w-full sm:w-auto sm:flex-grow sm:max-w-[150px]">
|
||||
<span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">
|
||||
VIP
|
||||
</span>
|
||||
<span className="text-sm font-medium text-foreground whitespace-nowrap hidden md:block">VIP</span>
|
||||
<Select value={vipFilter} onValueChange={setVipFilter}>
|
||||
<SelectTrigger className="w-full sm:w-32">
|
||||
{" "}
|
||||
{/* w-full para mobile, w-32 para sm+ */}
|
||||
<SelectTrigger className="w-full sm:w-32"> {/* w-full para mobile, w-32 para sm+ */}
|
||||
<SelectValue placeholder="VIP" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -241,11 +208,6 @@ export default function PacientesPage() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Aniversariantes - Ocupa 100% no mobile, e se alinha à direita no md+ */}
|
||||
<Button variant="outline" className="w-full md:w-auto md:ml-auto">
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
Aniversariantes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* --- SEÇÃO DE TABELA (VISÍVEL EM TELAS MAIORES OU IGUAIS A MD) --- */}
|
||||
|
||||
@ -1,159 +1,67 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { useState, useEffect, useMemo } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { FileText, Download, Eye, Calendar, User, X } from "lucide-react"
|
||||
import { FileText, Download, Eye, Calendar, User, X, Loader2 } from "lucide-react"
|
||||
import Sidebar from "@/components/Sidebar"
|
||||
import { useAuthLayout } from "@/hooks/useAuthLayout"
|
||||
import { reportsApi } from "@/services/reportsApi.mjs"
|
||||
|
||||
interface Report {
|
||||
id: string
|
||||
title: string
|
||||
doctor: string
|
||||
date: string
|
||||
type: string
|
||||
status: "disponivel" | "pendente"
|
||||
description: string
|
||||
content: {
|
||||
patientInfo: {
|
||||
name: string
|
||||
age: number
|
||||
gender: string
|
||||
id: string
|
||||
}
|
||||
examDetails: {
|
||||
requestingDoctor: string
|
||||
examDate: string
|
||||
reportDate: string
|
||||
technique: string
|
||||
}
|
||||
findings: string
|
||||
conclusion: string
|
||||
recommendations?: string
|
||||
}
|
||||
id: string;
|
||||
order_number: string;
|
||||
patient_id: string;
|
||||
status: string;
|
||||
exam: string;
|
||||
requested_by: string;
|
||||
cid_code: string;
|
||||
diagnosis: string;
|
||||
conclusion: string;
|
||||
content_html: string;
|
||||
content_json: any;
|
||||
hide_date: boolean;
|
||||
hide_signature: boolean;
|
||||
due_at: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export default function ReportsPage() {
|
||||
const [reports, setReports] = useState<Report[]>([])
|
||||
const [selectedReport, setSelectedReport] = useState<Report | null>(null)
|
||||
const [isViewModalOpen, setIsViewModalOpen] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const requiredRole = useMemo(() => ["paciente"], []);
|
||||
const { user, isLoading: isAuthLoading } = useAuthLayout({ requiredRole });
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const mockReports: Report[] = [
|
||||
{
|
||||
id: "1",
|
||||
title: "Exame de Sangue - Hemograma Completo",
|
||||
doctor: "Dr. João Silva",
|
||||
date: "2024-01-15",
|
||||
type: "Exame Laboratorial",
|
||||
status: "disponivel",
|
||||
description: "Hemograma completo com contagem de células sanguíneas",
|
||||
content: {
|
||||
patientInfo: {
|
||||
name: "Maria Silva Santos",
|
||||
age: 35,
|
||||
gender: "Feminino",
|
||||
id: "123.456.789-00",
|
||||
},
|
||||
examDetails: {
|
||||
requestingDoctor: "Dr. João Silva - CRM 12345",
|
||||
examDate: "15/01/2024",
|
||||
reportDate: "15/01/2024",
|
||||
technique: "Análise automatizada com confirmação microscópica",
|
||||
},
|
||||
findings:
|
||||
"Hemácias: 4.5 milhões/mm³ (VR: 4.0-5.2)\nHemoglobina: 13.2 g/dL (VR: 12.0-15.5)\nHematócrito: 40% (VR: 36-46)\nLeucócitos: 7.200/mm³ (VR: 4.000-11.000)\nPlaquetas: 280.000/mm³ (VR: 150.000-450.000)\n\nFórmula leucocitária:\n- Neutrófilos: 65% (VR: 50-70%)\n- Linfócitos: 28% (VR: 20-40%)\n- Monócitos: 5% (VR: 2-8%)\n- Eosinófilos: 2% (VR: 1-4%)",
|
||||
conclusion:
|
||||
"Hemograma dentro dos parâmetros normais. Não foram observadas alterações significativas na série vermelha, branca ou plaquetária.",
|
||||
recommendations: "Manter acompanhamento médico regular. Repetir exame conforme orientação médica.",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
title: "Radiografia do Tórax",
|
||||
doctor: "Dra. Maria Santos",
|
||||
date: "2024-01-10",
|
||||
type: "Exame de Imagem",
|
||||
status: "disponivel",
|
||||
description: "Radiografia PA e perfil do tórax",
|
||||
content: {
|
||||
patientInfo: {
|
||||
name: "Maria Silva Santos",
|
||||
age: 35,
|
||||
gender: "Feminino",
|
||||
id: "123.456.789-00",
|
||||
},
|
||||
examDetails: {
|
||||
requestingDoctor: "Dra. Maria Santos - CRM 67890",
|
||||
examDate: "10/01/2024",
|
||||
reportDate: "10/01/2024",
|
||||
technique: "Radiografia digital PA e perfil",
|
||||
},
|
||||
findings:
|
||||
"Campos pulmonares livres, sem sinais de consolidação ou derrame pleural. Silhueta cardíaca dentro dos limites normais. Estruturas ósseas íntegras. Diafragmas em posição normal.",
|
||||
conclusion: "Radiografia de tórax sem alterações patológicas evidentes.",
|
||||
recommendations: "Correlacionar com quadro clínico. Acompanhamento conforme indicação médica.",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
title: "Eletrocardiograma",
|
||||
doctor: "Dr. Carlos Oliveira",
|
||||
date: "2024-01-08",
|
||||
type: "Exame Cardiológico",
|
||||
status: "pendente",
|
||||
description: "ECG de repouso para avaliação cardíaca",
|
||||
content: {
|
||||
patientInfo: {
|
||||
name: "Maria Silva Santos",
|
||||
age: 35,
|
||||
gender: "Feminino",
|
||||
id: "123.456.789-00",
|
||||
},
|
||||
examDetails: {
|
||||
requestingDoctor: "Dr. Carlos Oliveira - CRM 54321",
|
||||
examDate: "08/01/2024",
|
||||
reportDate: "",
|
||||
technique: "ECG de repouso",
|
||||
},
|
||||
findings: "",
|
||||
conclusion: "",
|
||||
recommendations: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
title: "Ultrassom Abdominal",
|
||||
doctor: "Dra. Ana Costa",
|
||||
date: "2024-01-05",
|
||||
type: "Exame de Imagem",
|
||||
status: "disponivel",
|
||||
description: "Ultrassonografia do abdome total",
|
||||
content: {
|
||||
patientInfo: {
|
||||
name: "Maria Silva Santos",
|
||||
age: 35,
|
||||
gender: "Feminino",
|
||||
id: "123.456.789-00",
|
||||
},
|
||||
examDetails: {
|
||||
requestingDoctor: "Dra. Ana Costa - CRM 98765",
|
||||
examDate: "05/01/2024",
|
||||
reportDate: "05/01/2024",
|
||||
technique: "Ultrassom convencional",
|
||||
},
|
||||
findings:
|
||||
"Viscerais bem posicionadas. Rim direito e esquerdo com contornos normais. Vesícula com volume dentro do normal.",
|
||||
conclusion: "Ultrassom abdominal sem alterações patológicas evidentes.",
|
||||
recommendations: "Acompanhamento conforme indicação médica.",
|
||||
},
|
||||
},
|
||||
]
|
||||
setReports(mockReports)
|
||||
}, [])
|
||||
if (user) {
|
||||
const fetchReports = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const fetchedReports = await reportsApi.getReports(user.id);
|
||||
setReports(fetchedReports);
|
||||
} catch (error) {
|
||||
console.error("Erro ao buscar laudos:", error)
|
||||
toast({
|
||||
title: "Erro ao buscar laudos",
|
||||
description: "Não foi possível carregar os laudos. Tente novamente.",
|
||||
variant: "destructive",
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
fetchReports()
|
||||
}
|
||||
}, [user?.id])
|
||||
|
||||
const handleViewReport = (reportId: string) => {
|
||||
const report = reports.find((r) => r.id === reportId)
|
||||
@ -168,100 +76,23 @@ export default function ReportsPage() {
|
||||
if (!report) return
|
||||
|
||||
try {
|
||||
// Simular loading
|
||||
toast({
|
||||
title: "Preparando download...",
|
||||
description: "Gerando PDF do laudo médico",
|
||||
})
|
||||
|
||||
// Criar conteúdo HTML do laudo para conversão em PDF
|
||||
const htmlContent = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Laudo Médico - ${report.title}</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; line-height: 1.6; }
|
||||
.header { text-align: center; border-bottom: 2px solid #333; padding-bottom: 20px; margin-bottom: 30px; }
|
||||
.section { margin-bottom: 25px; }
|
||||
.section-title { font-size: 16px; font-weight: bold; color: #333; margin-bottom: 10px; border-bottom: 1px solid #ccc; padding-bottom: 5px; }
|
||||
.info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px; }
|
||||
.info-item { margin-bottom: 8px; }
|
||||
.label { font-weight: bold; color: #555; }
|
||||
.content { white-space: pre-line; }
|
||||
.footer { margin-top: 40px; text-align: center; font-size: 12px; color: #666; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>LAUDO MÉDICO</h1>
|
||||
<h2>${report.title}</h2>
|
||||
<p><strong>Tipo:</strong> ${report.type}</p>
|
||||
</div>
|
||||
const htmlContent = report.content_html;
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">DADOS DO PACIENTE</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item"><span class="label">Nome:</span> ${report.content.patientInfo.name}</div>
|
||||
<div class="info-item"><span class="label">Idade:</span> ${report.content.patientInfo.age} anos</div>
|
||||
<div class="info-item"><span class="label">Sexo:</span> ${report.content.patientInfo.gender}</div>
|
||||
<div class="info-item"><span class="label">CPF:</span> ${report.content.patientInfo.id}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">DETALHES DO EXAME</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-item"><span class="label">Médico Solicitante:</span> ${report.content.examDetails.requestingDoctor}</div>
|
||||
<div class="info-item"><span class="label">Data do Exame:</span> ${report.content.examDetails.examDate}</div>
|
||||
<div class="info-item"><span class="label">Data do Laudo:</span> ${report.content.examDetails.reportDate}</div>
|
||||
<div class="info-item"><span class="label">Técnica:</span> ${report.content.examDetails.technique}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">ACHADOS</div>
|
||||
<div class="content">${report.content.findings}</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">CONCLUSÃO</div>
|
||||
<div class="content">${report.content.conclusion}</div>
|
||||
</div>
|
||||
|
||||
${
|
||||
report.content.recommendations
|
||||
? `
|
||||
<div class="section">
|
||||
<div class="section-title">RECOMENDAÇÕES</div>
|
||||
<div class="content">${report.content.recommendations}</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
|
||||
<div class="footer">
|
||||
<p>Documento gerado em ${new Date().toLocaleDateString("pt-BR")} às ${new Date().toLocaleTimeString("pt-BR")}</p>
|
||||
<p>Este é um documento médico oficial. Mantenha-o em local seguro.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
// Criar blob com o conteúdo HTML
|
||||
const blob = new Blob([htmlContent], { type: "text/html" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
// Criar link temporário para download
|
||||
const link = document.createElement("a")
|
||||
link.href = url
|
||||
link.download = `laudo-${report.title.replace(/[^a-zA-Z0-9]/g, "-").toLowerCase()}-${report.date}.html`
|
||||
link.download = `laudo-${report.order_number}.html`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
|
||||
// Limpar URL temporária
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
toast({
|
||||
@ -283,8 +114,18 @@ export default function ReportsPage() {
|
||||
setSelectedReport(null)
|
||||
}
|
||||
|
||||
const availableReports = reports.filter((report) => report.status === "disponivel")
|
||||
const pendingReports = reports.filter((report) => report.status === "pendente")
|
||||
const availableReports = reports.filter((report) => report.status.toLowerCase() === "draft")
|
||||
const pendingReports = reports.filter((report) => report.status.toLowerCase() !== "draft")
|
||||
|
||||
if (isLoading || isAuthLoading) {
|
||||
return (
|
||||
<Sidebar>
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
</Sidebar>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
@ -333,25 +174,25 @@ export default function ReportsPage() {
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-lg">{report.title}</CardTitle>
|
||||
<CardTitle className="text-lg">{report.exam}</CardTitle>
|
||||
<CardDescription className="flex items-center gap-4">
|
||||
<span className="flex items-center gap-1">
|
||||
<User className="h-4 w-4" />
|
||||
{report.doctor}
|
||||
{report.requested_by}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{new Date(report.date).toLocaleDateString("pt-BR")}
|
||||
{new Date(report.created_at).toLocaleDateString("pt-BR")}
|
||||
</span>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary" className="bg-green-100 text-green-800">
|
||||
{report.type}
|
||||
Finalizado
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-600 mb-4">{report.description}</p>
|
||||
<p className="text-gray-600 mb-4">{report.diagnosis}</p>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
@ -369,7 +210,7 @@ export default function ReportsPage() {
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
Baixar PDF
|
||||
Baixar
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@ -388,25 +229,25 @@ export default function ReportsPage() {
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-lg">{report.title}</CardTitle>
|
||||
<CardTitle className="text-lg">{report.exam}</CardTitle>
|
||||
<CardDescription className="flex items-center gap-4">
|
||||
<span className="flex items-center gap-1">
|
||||
<User className="h-4 w-4" />
|
||||
{report.doctor}
|
||||
{report.requested_by}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
{new Date(report.date).toLocaleDateString("pt-BR")}
|
||||
{new Date(report.created_at).toLocaleDateString("pt-BR")}
|
||||
</span>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge variant="secondary" className="bg-yellow-100 text-yellow-800">
|
||||
Pendente
|
||||
{report.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-gray-600 mb-4">{report.description}</p>
|
||||
<p className="text-gray-600 mb-4">{report.diagnosis}</p>
|
||||
<p className="text-sm text-yellow-600 font-medium">
|
||||
Laudo em processamento. Você será notificado quando estiver disponível.
|
||||
</p>
|
||||
@ -417,7 +258,7 @@ export default function ReportsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{reports.length === 0 && (
|
||||
{reports.length === 0 && !isLoading && (
|
||||
<Card className="text-center py-12">
|
||||
<CardContent>
|
||||
<FileText className="h-12 w-12 text-gray-400 mx-auto mb-4" />
|
||||
@ -432,9 +273,9 @@ export default function ReportsPage() {
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<DialogTitle className="text-xl font-bold">{selectedReport?.title}</DialogTitle>
|
||||
<DialogTitle className="text-xl font-bold">{selectedReport?.exam}</DialogTitle>
|
||||
<DialogDescription className="mt-1">
|
||||
{selectedReport?.type} - {selectedReport?.doctor}
|
||||
{selectedReport?.order_number}
|
||||
</DialogDescription>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={handleCloseModal} className="h-8 w-8 p-0">
|
||||
@ -444,94 +285,7 @@ export default function ReportsPage() {
|
||||
</DialogHeader>
|
||||
|
||||
{selectedReport && (
|
||||
<div className="space-y-6 mt-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Dados do Paciente</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">Nome</p>
|
||||
<p className="text-sm">{selectedReport.content.patientInfo.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">Idade</p>
|
||||
<p className="text-sm">{selectedReport.content.patientInfo.age} anos</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">Sexo</p>
|
||||
<p className="text-sm">{selectedReport.content.patientInfo.gender}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">CPF</p>
|
||||
<p className="text-sm">{selectedReport.content.patientInfo.id}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Detalhes do Exame</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">Médico Solicitante</p>
|
||||
<p className="text-sm">{selectedReport.content.examDetails.requestingDoctor}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">Data do Exame</p>
|
||||
<p className="text-sm">{selectedReport.content.examDetails.examDate}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">Data do Laudo</p>
|
||||
<p className="text-sm">{selectedReport.content.examDetails.reportDate}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-500">Técnica</p>
|
||||
<p className="text-sm">{selectedReport.content.examDetails.technique}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Achados</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="whitespace-pre-line text-sm leading-relaxed">{selectedReport.content.findings}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Conclusão</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm leading-relaxed">{selectedReport.content.conclusion}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{selectedReport.content.recommendations && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Recomendações</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm leading-relaxed">{selectedReport.content.recommendations}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="flex gap-3 pt-4 border-t">
|
||||
<Button onClick={() => handleDownloadReport(selectedReport.id)} className="flex items-center gap-2">
|
||||
<Download className="h-4 w-4" />
|
||||
Baixar PDF
|
||||
</Button>
|
||||
<Button variant="outline" onClick={handleCloseModal}>
|
||||
Fechar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-6 mt-4" dangerouslySetInnerHTML={{ __html: selectedReport.content_html }} />
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -8,7 +8,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Plus, Edit, Trash2, Eye, Calendar, Filter, Loader2, MoreVertical } from "lucide-react";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
|
||||
import SecretaryLayout from "@/components/secretary-layout";
|
||||
import { patientsService } from "@/services/patientsApi.mjs";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
@ -418,10 +417,7 @@ export default function PacientesPage() {
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
href={`/secretary/pacientes/${patient.id}/editar`}
|
||||
className="flex items-center w-full"
|
||||
>
|
||||
<Link href={`/secretary/pacientes/${patient.id}/editar`} className="flex items-center w-full">
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
Editar
|
||||
</Link>
|
||||
@ -431,10 +427,7 @@ export default function PacientesPage() {
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
Marcar consulta
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-red-600"
|
||||
onClick={() => openDeleteDialog(String(patient.id))}
|
||||
>
|
||||
<DropdownMenuItem className="text-red-600" onClick={() => openDeleteDialog(String(patient.id))}>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Excluir
|
||||
</DropdownMenuItem>
|
||||
@ -449,9 +442,7 @@ export default function PacientesPage() {
|
||||
{/* Paginação */}
|
||||
{totalPages > 1 && !loading && (
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center p-4 border-t border-gray-200">
|
||||
<div className="flex space-x-2 flex-wrap justify-center">
|
||||
{" "}
|
||||
{/* Adicionado flex-wrap e justify-center para botões da paginação */}
|
||||
<div className="flex space-x-2 flex-wrap justify-center"> {/* Adicionado flex-wrap e justify-center para botões da paginação */}
|
||||
<Button
|
||||
onClick={() => setPage((prev) => Math.max(1, prev - 1))}
|
||||
disabled={page === 1}
|
||||
@ -460,6 +451,7 @@ export default function PacientesPage() {
|
||||
>
|
||||
< Anterior
|
||||
</Button>
|
||||
|
||||
{Array.from({ length: totalPages }, (_, index) => index + 1)
|
||||
.slice(Math.max(0, page - 3), Math.min(totalPages, page + 2))
|
||||
.map((pageNumber) => (
|
||||
@ -468,19 +460,14 @@ export default function PacientesPage() {
|
||||
onClick={() => setPage(pageNumber)}
|
||||
variant={pageNumber === page ? "default" : "outline"}
|
||||
size="lg"
|
||||
className={
|
||||
pageNumber === page
|
||||
? "bg-blue-600 hover:bg-blue-700 text-white"
|
||||
: "text-gray-700"
|
||||
}
|
||||
className={pageNumber === page ? "bg-green-600 hover:bg-green-700 text-white" : "text-gray-700"}
|
||||
>
|
||||
{pageNumber}
|
||||
</Button>
|
||||
))}
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
setPage((prev) => Math.min(totalPages, prev + 1))
|
||||
}
|
||||
onClick={() => setPage((prev) => Math.min(totalPages, prev + 1))}
|
||||
disabled={page === totalPages}
|
||||
variant="outline"
|
||||
size="lg"
|
||||
@ -496,19 +483,11 @@ export default function PacientesPage() {
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Confirmar exclusão</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Tem certeza que deseja excluir este paciente? Esta ação não pode
|
||||
ser desfeita.
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogDescription>Tem certeza que deseja excluir este paciente? Esta ação não pode ser desfeita.</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancelar</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() =>
|
||||
patientToDelete && handleDeletePatient(patientToDelete)
|
||||
}
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
>
|
||||
<AlertDialogAction onClick={() => patientToDelete && handleDeletePatient(patientToDelete)} className="bg-red-600 hover:bg-red-700">
|
||||
Excluir
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
|
||||
@ -9,7 +9,7 @@ export const reportsApi = {
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch reports:", error);
|
||||
return [];
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
getReportById: async (reportId) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user