"use client"; import React, { useState, useRef, useEffect } from "react"; import Image from "next/image"; import SignatureCanvas from "react-signature-canvas"; import Link from "next/link"; import ProtectedRoute from "@/components/shared/ProtectedRoute"; import { useAuth } from "@/hooks/useAuth"; import { useToast } from "@/hooks/use-toast"; import { buscarPacientes, listarPacientes, buscarPacientePorId, buscarPacientesPorIds, buscarMedicoPorId, buscarMedicosPorIds, buscarMedicos, listarAgendamentos, type Paciente, buscarRelatorioPorId, atualizarMedico } from "@/lib/api"; import { useReports } from "@/hooks/useReports"; import { CreateReportData } from "@/types/report-types"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectTrigger, SelectContent, SelectItem, SelectValue } from "@/components/ui/select"; import { SimpleThemeToggle } from "@/components/ui/simple-theme-toggle"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar" import { User, FolderOpen, X, Users, MessageSquare, ClipboardList, Plus, Edit, Trash2, ChevronLeft, ChevronRight, Clock, FileCheck, Upload, Download, Eye, History, Stethoscope, Pill, Activity, Search } from "lucide-react" import { Calendar as CalendarIcon, FileText, Settings } from "lucide-react"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; import dynamic from "next/dynamic"; import { ENV_CONFIG } from '@/lib/env-config'; import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import ptBrLocale from "@fullcalendar/core/locales/pt-br"; const FullCalendar = dynamic(() => import("@fullcalendar/react"), { ssr: false, }); // pacientes will be loaded inside the component (hooks must run in component body) // removed static medico placeholder; will load real profile for logged-in user const colorsByType = { Rotina: "#4dabf7", Cardiologia: "#f76c6c", Otorrino: "#f7b84d", Pediatria: "#6cf78b", Dermatologia: "#9b59b6", Oftalmologia: "#2ecc71" }; // Helpers para normalizar dados de paciente (suporta schema antigo e novo) const getPatientName = (p: any) => p?.full_name ?? p?.nome ?? ''; const getPatientCpf = (p: any) => p?.cpf ?? ''; const getPatientSex = (p: any) => p?.sex ?? p?.sexo ?? ''; const getPatientId = (p: any) => p?.id ?? ''; const getPatientAge = (p: any) => { if (!p) return ''; // Prefer birth_date (ISO) to calcular idade const bd = p?.birth_date ?? p?.data_nascimento ?? p?.birthDate; if (bd) { const d = new Date(bd); if (!isNaN(d.getTime())) { const age = Math.floor((Date.now() - d.getTime()) / (1000 * 60 * 60 * 24 * 365.25)); return `${age}`; } } // Fallback para campo idade/idade_anterior return p?.idade ?? p?.age ?? ''; }; // Normaliza número de telefone para E.164 básico (prioriza +55 quando aplicável) const normalizePhoneNumber = (raw?: string) => { if (!raw || typeof raw !== 'string') return ''; // Remover tudo que não for dígito const digits = raw.replace(/\D+/g, ''); if (!digits) return ''; // Já tem código de país (começa com 55) if (digits.startsWith('55') && digits.length >= 11) return '+' + digits; // Se tiver 10 ou 11 dígitos (DDD + número), assume Brasil e prefixa +55 if (digits.length === 10 || digits.length === 11) return '+55' + digits; // Se tiver outros formatos pequenos, apenas prefixa + return '+' + digits; }; // Helpers para normalizar campos do laudo/relatório const getReportPatientName = (r: any) => r?.paciente?.full_name ?? r?.paciente?.nome ?? r?.patient?.full_name ?? r?.patient?.nome ?? r?.patient_name ?? r?.patient_full_name ?? ''; const getReportPatientId = (r: any) => r?.paciente?.id ?? r?.patient?.id ?? r?.patient_id ?? r?.patientId ?? r?.patient_id_raw ?? r?.patient_id ?? r?.id ?? ''; const getReportPatientCpf = (r: any) => r?.paciente?.cpf ?? r?.patient?.cpf ?? r?.patient_cpf ?? ''; const getReportExecutor = (r: any) => r?.executante ?? r?.requested_by ?? r?.requestedBy ?? r?.created_by ?? r?.createdBy ?? r?.requested_by_name ?? r?.executor ?? ''; const getReportExam = (r: any) => r?.exame ?? r?.exam ?? r?.especialidade ?? r?.cid_code ?? r?.report_type ?? '-'; const getReportDate = (r: any) => r?.data ?? r?.created_at ?? r?.due_at ?? r?.report_date ?? ''; const formatReportDate = (raw?: string) => { if (!raw) return '-'; try { const d = new Date(raw); if (isNaN(d.getTime())) return raw; return d.toLocaleDateString('pt-BR'); } catch (e) { return raw; } }; const ProfissionalPage = () => { const { logout, user, token } = useAuth(); const [activeSection, setActiveSection] = useState('calendario'); const [pacienteSelecionado, setPacienteSelecionado] = useState(null); // Estados para edição de laudo const [isEditingLaudoForPatient, setIsEditingLaudoForPatient] = useState(false); const [patientForLaudo, setPatientForLaudo] = useState(null); // Estados para o perfil do médico const [isEditingProfile, setIsEditingProfile] = useState(false); const [doctorId, setDoctorId] = useState(null); // Removemos o placeholder extenso — inicializamos com valores minimalistas e vazios. const [profileData, setProfileData] = useState({ nome: '', email: user?.email || '', telefone: '', endereco: '', cidade: '', cep: '', crm: '', especialidade: '', // biografia field removed — not present in Medico records fotoUrl: '' }); // pacientes carregados dinamicamente (hooks devem ficar dentro do componente) const [pacientes, setPacientes] = useState([]); useEffect(() => { let mounted = true; (async () => { try { if (!user || !user.id) { if (mounted) setPacientes([]); return; } const assignmentsMod = await import('@/lib/assignment'); if (!assignmentsMod || typeof assignmentsMod.listAssignmentsForUser !== 'function') { if (mounted) setPacientes([]); return; } const assignments = await assignmentsMod.listAssignmentsForUser(user.id || ''); const patientIds = Array.isArray(assignments) ? assignments.map((a:any) => String(a.patient_id)).filter(Boolean) : []; if (!patientIds.length) { if (mounted) setPacientes([]); return; } const patients = await buscarPacientesPorIds(patientIds); const normalized = (patients || []).map((p: any) => ({ ...p, nome: p.full_name ?? (p as any).nome ?? '', cpf: p.cpf ?? '', idade: getPatientAge(p) // preencher idade para a tabela de pacientes })); if (mounted) setPacientes(normalized); } catch (err) { console.warn('[ProfissionalPage] falha ao carregar pacientes atribuídos:', err); if (mounted) setPacientes([]); } })(); return () => { mounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Carregar perfil do médico correspondente ao usuário logado useEffect(() => { let mounted = true; (async () => { try { if (!user || !user.email) return; // Tenta buscar médicos pelo email do usuário (buscarMedicos lida com queries por email) const docs = await buscarMedicos(user.email); if (!mounted) return; if (Array.isArray(docs) && docs.length > 0) { // preferir registro cujo user_id bate com user.id let chosen = docs.find(d => String((d as any).user_id) === String(user.id)) || docs[0]; if (chosen) { // store the doctor's id so we can update it later try { setDoctorId((chosen as any).id ?? null); } catch {}; // Especialidade pode vir como 'specialty' (inglês), 'especialidade' (pt), // ou até uma lista/array. Normalizamos para string. const rawSpecialty = (chosen as any).specialty ?? (chosen as any).especialidade ?? (chosen as any).especialidades ?? (chosen as any).especiality; let specialtyStr = ''; if (Array.isArray(rawSpecialty)) { specialtyStr = rawSpecialty.join(', '); } else if (rawSpecialty) { specialtyStr = String(rawSpecialty); } // Foto pode vir como 'foto_url' ou 'fotoUrl' ou 'avatar_url' const foto = (chosen as any).foto_url || (chosen as any).fotoUrl || (chosen as any).avatar_url || ''; setProfileData((prev) => ({ ...prev, nome: (chosen as any).full_name || (chosen as any).nome_social || prev.nome || user?.email?.split('@')[0] || '', email: (chosen as any).email || user?.email || prev.email, telefone: (chosen as any).phone_mobile || (chosen as any).celular || (chosen as any).telefone || (chosen as any).phone || (chosen as any).mobile || (user as any)?.user_metadata?.phone || prev.telefone, endereco: (chosen as any).street || (chosen as any).endereco || prev.endereco, cidade: (chosen as any).city || (chosen as any).cidade || prev.cidade, cep: (chosen as any).cep || prev.cep, // store raw CRM (only the number) to avoid double-prefixing when rendering crm: (chosen as any).crm ? String((chosen as any).crm).replace(/^(?:CRM\s*)+/i, '').trim() : (prev.crm || ''), especialidade: specialtyStr || prev.especialidade || '', // biografia removed: prefer to ignore observacoes/curriculo_url here // (if needed elsewhere, render directly from chosen.observacoes) fotoUrl: foto || prev.fotoUrl || '' })); } } } catch (e) { console.warn('[ProfissionalPage] falha ao carregar perfil do médico pelo email:', e); } })(); return () => { mounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Estados para campos principais da consulta const [consultaAtual, setConsultaAtual] = useState({ patient_id: "", order_number: "", exam: "", diagnosis: "", conclusion: "", cid_code: "", content_html: "", content_json: {}, status: "draft", requested_by: "", due_at: new Date().toISOString(), hide_date: true, hide_signature: true }); const [events, setEvents] = useState([]); // Load real appointments for the logged in doctor and map to calendar events useEffect(() => { let mounted = true; (async () => { try { // If we already have a doctorId (set earlier), use it. Otherwise try to resolve from the logged user let docId = doctorId; if (!docId && user && user.email) { // buscarMedicos may return the doctor's record including id try { const docs = await buscarMedicos(user.email).catch(() => []); if (Array.isArray(docs) && docs.length > 0) { const chosen = docs.find(d => String((d as any).user_id) === String(user.id)) || docs[0]; docId = (chosen as any)?.id ?? null; if (mounted && !doctorId) setDoctorId(docId); } } catch (e) { // ignore } } if (!docId) { // nothing to fetch yet return; } // Fetch appointments for this doctor. We'll ask for future and recent past appointments // using a simple filter: doctor_id=eq.&order=scheduled_at.asc&limit=200 const qs = `?select=*&doctor_id=eq.${encodeURIComponent(String(docId))}&order=scheduled_at.asc&limit=200`; const appts = await listarAgendamentos(qs).catch(() => []); if (!mounted) return; // Enrich appointments with patient names (batch fetch) and map to UI events const patientIds = Array.from(new Set((appts || []).map((x:any) => String(x.patient_id || x.patient_id_raw || '').trim()).filter(Boolean))); let patientMap = new Map(); if (patientIds.length) { try { const patients = await buscarPacientesPorIds(patientIds).catch(() => []); for (const p of patients || []) { if (p && p.id) patientMap.set(String(p.id), p); } } catch (e) { console.warn('[ProfissionalPage] falha ao buscar pacientes para eventos:', e); } } const mapped = (appts || []).map((a: any, idx: number) => { const scheduled = a.scheduled_at || a.time || a.created_at || null; // Use local date components to avoid UTC shift when showing the appointment day/time let datePart = new Date().toISOString().split('T')[0]; let timePart = ''; if (scheduled) { try { const d = new Date(scheduled); // build local date string YYYY-MM-DD using local getters const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); datePart = `${y}-${m}-${day}`; timePart = `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`; } catch (e) { // ignore } } const pid = a.patient_id || a.patient || a.patient_id_raw || a.patientId || null; const patientObj = pid ? patientMap.get(String(pid)) : null; const patientName = patientObj?.full_name || a.patient || a.patient_name || String(pid) || 'Paciente'; const patientIdVal = pid || null; return { id: a.id ?? `srv-${idx}-${String(a.scheduled_at || a.created_at || idx)}`, title: patientName || 'Paciente', type: a.appointment_type || 'Consulta', time: timePart || '', date: datePart, pacienteId: patientIdVal, color: colorsByType[a.specialty as keyof typeof colorsByType] || '#4dabf7', raw: a, }; }); setEvents(mapped); // Helper: parse 'YYYY-MM-DD' into a local Date to avoid UTC parsing which can shift day const parseYMDToLocal = (ymd?: string) => { if (!ymd || typeof ymd !== 'string') return new Date(); const parts = ymd.split('-').map(Number); if (parts.length < 3 || parts.some((n) => Number.isNaN(n))) return new Date(ymd); const [y, m, d] = parts; return new Date(y, (m || 1) - 1, d || 1); }; // Set calendar view to nearest upcoming appointment (or today) try { const now = Date.now(); const upcoming = mapped.find((m:any) => { if (!m.raw) return false; const s = m.raw.scheduled_at || m.raw.time || m.raw.created_at; if (!s) return false; const t = new Date(s).getTime(); return !isNaN(t) && t >= now; }); if (upcoming) { setCurrentCalendarDate(parseYMDToLocal(upcoming.date)); } else if (mapped.length) { // fallback: show the date of the first appointment setCurrentCalendarDate(parseYMDToLocal(mapped[0].date)); } } catch (e) { // ignore } } catch (err) { console.warn('[ProfissionalPage] falha ao carregar agendamentos do servidor:', err); // Keep mocked/empty events if fetch fails } })(); return () => { mounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [doctorId]); const [editingEvent, setEditingEvent] = useState(null); const [showPopup, setShowPopup] = useState(false); const [showActionModal, setShowActionModal] = useState(false); const [step, setStep] = useState(1); const [newEvent, setNewEvent] = useState({ title: "", type: "", time: "", pacienteId: "" }); const [selectedDate, setSelectedDate] = useState(null); const [selectedEvent, setSelectedEvent] = useState(null); const [currentCalendarDate, setCurrentCalendarDate] = useState(new Date()); const [commPhoneNumber, setCommPhoneNumber] = useState(''); const [commMessage, setCommMessage] = useState(''); const [commPatientId, setCommPatientId] = useState(null); const [smsSending, setSmsSending] = useState(false); const handleSave = async (event: React.MouseEvent) => { event.preventDefault(); setSmsSending(true); try { // Validate required fields if (!commPhoneNumber || !commPhoneNumber.trim()) throw new Error('O campo phone_number é obrigatório'); if (!commMessage || !commMessage.trim()) throw new Error('O campo message é obrigatório'); const payload: any = { phone_number: commPhoneNumber.trim(), message: commMessage.trim() }; if (commPatientId) payload.patient_id = commPatientId; const headers: Record = { 'Content-Type': 'application/json' }; // include any default headers from ENV_CONFIG if present (e.g. apikey) if ((ENV_CONFIG as any)?.DEFAULT_HEADERS) Object.assign(headers, (ENV_CONFIG as any).DEFAULT_HEADERS); // include Authorization if we have a token (user session) if (token) headers['Authorization'] = `Bearer ${token}`; // Ensure apikey is present (frontend only has ANON key in this project) if (!headers.apikey && (ENV_CONFIG as any)?.SUPABASE_ANON_KEY) { headers.apikey = (ENV_CONFIG as any).SUPABASE_ANON_KEY; } // Ensure Accept header headers['Accept'] = 'application/json'; // Normalizar número antes de enviar (E.164 básico) const normalized = normalizePhoneNumber(commPhoneNumber); if (!normalized) throw new Error('Número inválido após normalização'); payload.phone_number = normalized; // Debug: log payload and headers with secrets masked to help diagnose issues try { const masked = { ...headers } as Record; if (masked.apikey && typeof masked.apikey === 'string') masked.apikey = `${masked.apikey.slice(0,4)}...${masked.apikey.slice(-4)}`; if (masked.Authorization) masked.Authorization = 'Bearer <>'; console.debug('[ProfissionalPage] Enviando SMS -> url:', `${(ENV_CONFIG as any).SUPABASE_URL}/functions/v1/send-sms`, 'payload:', payload, 'headers(masked):', masked); } catch (e) { // ignore logging errors } const res = await fetch(`${(ENV_CONFIG as any).SUPABASE_URL}/functions/v1/send-sms`, { method: 'POST', headers, body: JSON.stringify(payload), }); const body = await res.json().catch(() => null); if (!res.ok) { // If server returned 5xx and we sent a patient_id, try a single retry without patient_id if (res.status >= 500 && payload.patient_id) { try { const fallback = { phone_number: payload.phone_number, message: payload.message }; console.debug('[ProfissionalPage] 5xx ao enviar com patient_id — tentando reenviar sem patient_id', { fallback }); const retryRes = await fetch(`${(ENV_CONFIG as any).SUPABASE_URL}/functions/v1/send-sms`, { method: 'POST', headers, body: JSON.stringify(fallback), }); const retryBody = await retryRes.json().catch(() => null); if (retryRes.ok) { alert('SMS enviado com sucesso (sem patient_id)'); setCommPhoneNumber(''); setCommMessage(''); setCommPatientId(null); return; } else { throw new Error(retryBody?.message || retryBody?.error || `Erro ao enviar SMS (retry ${retryRes.status})`); } } catch (retryErr) { console.warn('[ProfissionalPage] Reenvio sem patient_id falhou', retryErr); throw new Error(body?.message || body?.error || `Erro ao enviar SMS (${res.status})`); } } throw new Error(body?.message || body?.error || `Erro ao enviar SMS (${res.status})`); } // success feedback alert('SMS enviado com sucesso'); // clear fields setCommPhoneNumber(''); setCommMessage(''); setCommPatientId(null); } catch (err: any) { alert(String(err?.message || err || 'Falha ao enviar SMS')); } finally { setSmsSending(false); window.scrollTo({ top: 0, behavior: 'smooth' }); } }; const handleEditarLaudo = (paciente: any) => { setPatientForLaudo(paciente); setIsEditingLaudoForPatient(true); setActiveSection('laudos'); }; const navigateDate = (direction: 'prev' | 'next') => { const newDate = new Date(currentCalendarDate); newDate.setDate(newDate.getDate() + (direction === 'next' ? 1 : -1)); setCurrentCalendarDate(newDate); }; const goToToday = () => { setCurrentCalendarDate(new Date()); }; const formatDate = (date: Date) => { return date.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); }; // Filtrar eventos do dia atual const getTodayEvents = () => { const today = currentCalendarDate.toISOString().split('T')[0]; return events .filter(event => event.date === today) .sort((a, b) => a.time.localeCompare(b.time)); }; const getStatusColor = (type: string) => { return colorsByType[type as keyof typeof colorsByType] || "#4dabf7"; }; // Funções para o perfil const handleProfileChange = (field: string, value: string) => { setProfileData(prev => ({ ...prev, [field]: value })); }; const handleSaveProfile = () => { (async () => { if (!doctorId) { alert('Não foi possível localizar o registro do médico para atualizar.'); setIsEditingProfile(false); return; } // Build payload mapping UI fields to DB columns const payload: any = {}; if (profileData.email) payload.email = profileData.email; if (profileData.telefone) payload.phone_mobile = profileData.telefone; if (profileData.endereco) payload.street = profileData.endereco; if (profileData.cidade) payload.city = profileData.cidade; if (profileData.cep) payload.cep = profileData.cep; if (profileData.especialidade) payload.specialty = profileData.especialidade || profileData.especialidade; if (profileData.fotoUrl) payload.foto_url = profileData.fotoUrl; // Don't allow updating full_name or crm from this UI try { const updated = await atualizarMedico(doctorId, payload as any); console.debug('[ProfissionalPage] médico atualizado:', updated); alert('Perfil atualizado com sucesso!'); } catch (err: any) { console.error('[ProfissionalPage] falha ao atualizar médico:', err); // Mostrar mensagem amigável (o erro já é tratado em lib/api) alert(err?.message || 'Falha ao atualizar perfil. Verifique logs.'); } finally { setIsEditingProfile(false); } })(); }; const handleCancelEdit = () => { setIsEditingProfile(false); }; const handleDateClick = (arg: any) => { setSelectedDate(arg.dateStr); setNewEvent({ title: "", type: "", time: "", pacienteId: "" }); setStep(1); setEditingEvent(null); setShowPopup(true); }; const handleAddEvent = () => { const paciente = pacientes.find(p => p.nome === newEvent.title); const eventToAdd = { id: Date.now(), title: newEvent.title, type: newEvent.type, time: newEvent.time, date: selectedDate || currentCalendarDate.toISOString().split('T')[0], pacienteId: paciente ? paciente.cpf : "", color: colorsByType[newEvent.type as keyof typeof colorsByType] || "#4dabf7" }; setEvents((prev) => [...prev, eventToAdd]); setShowPopup(false); }; const handleEditEvent = () => { setEvents((prevEvents) => prevEvents.map((ev) => ev.id.toString() === editingEvent.id.toString() ? { ...ev, title: newEvent.title, type: newEvent.type, time: newEvent.time, color: colorsByType[newEvent.type as keyof typeof colorsByType] || "#4dabf7" } : ev ) ); setEditingEvent(null); setShowPopup(false); setShowActionModal(false); }; const handleNextStep = () => { if (step < 3) setStep(step + 1); else editingEvent ? handleEditEvent() : handleAddEvent(); }; const handleEventClick = (clickInfo: any) => { setSelectedEvent(clickInfo.event); setShowActionModal(true); }; const handleDeleteEvent = () => { if (!selectedEvent) return; setEvents((prevEvents) => prevEvents.filter((ev: any) => ev.id.toString() !== selectedEvent.id.toString()) ); setShowActionModal(false); }; const handleStartEdit = () => { if (!selectedEvent) return; setEditingEvent(selectedEvent); setNewEvent({ title: selectedEvent.title, type: selectedEvent.extendedProps.type, time: selectedEvent.extendedProps.time, pacienteId: selectedEvent.extendedProps.pacienteId || "" }); setStep(1); setShowActionModal(false); setShowPopup(true); }; const renderEventContent = (eventInfo: any) => { const bg = eventInfo.event.backgroundColor || eventInfo.event.extendedProps?.color || "#4dabf7"; return (
{eventInfo.event.title} {eventInfo.event.extendedProps.type} {eventInfo.event.extendedProps.time}
); }; const renderCalendarioSection = () => { const todayEvents = getTodayEvents(); return (

Agenda do Dia

{/* Navegação de Data */}

{formatDate(currentCalendarDate)}

{todayEvents.length} consulta{todayEvents.length !== 1 ? 's' : ''} agendada{todayEvents.length !== 1 ? 's' : ''}
{/* Lista de Pacientes do Dia */}
{todayEvents.length === 0 ? (

Nenhuma consulta agendada para este dia

Agenda livre para este dia

) : ( todayEvents.map((appointment) => { const paciente = pacientes.find(p => p.nome === appointment.title); return (
{appointment.title}
{paciente && (
CPF: {getPatientCpf(paciente)} • {getPatientAge(paciente)} anos
)}
{appointment.time}
{appointment.type}
Ver informações do paciente
); }) )}
); }; const renderLaudosSection = () => (
{ setIsEditingLaudoForPatient(false); setPatientForLaudo(null); }} />
); // --- NOVO SISTEMA DE LAUDOS COMPLETO --- function LaudoManager({ isEditingForPatient, selectedPatientForLaudo, onClosePatientEditor }: { isEditingForPatient?: boolean; selectedPatientForLaudo?: any; onClosePatientEditor?: () => void }) { const [pacientesDisponiveis] = useState([ { id: "95170038", nome: "Ana Souza", cpf: "123.456.789-00", idade: 42, sexo: "Feminino" }, { id: "93203056", nome: "Bruno Lima", cpf: "987.654.321-00", idade: 33, sexo: "Masculino" }, { id: "92953542", nome: "Carla Menezes", cpf: "111.222.333-44", idade: 67, sexo: "Feminino" }, ]); const { reports, loadReports, loadReportById, loading: reportsLoading, createNewReport, updateExistingReport } = useReports(); const [laudos, setLaudos] = useState([]); const [selectedRange, setSelectedRange] = useState<'todos'|'semana'|'mes'|'custom'>('mes'); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); // helper to check if a date string is in range const isInRange = (dateStr: string | undefined, range: 'todos'|'semana'|'mes'|'custom') => { if (range === 'todos') return true; if (!dateStr) return false; const d = new Date(dateStr); if (isNaN(d.getTime())) return false; const now = new Date(); if (range === 'semana') { const start = new Date(now); start.setDate(now.getDate() - now.getDay()); // sunday start const end = new Date(start); end.setDate(start.getDate() + 6); return d >= start && d <= end; } // mes return d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth(); }; // helper: ensure report has paciente object populated (fetch by id if necessary) const ensurePaciente = async (report: any) => { if (!report) return report; try { if (!report.paciente) { const pid = report.patient_id ?? report.patient ?? report.paciente ?? null; if (pid) { try { const p = await buscarPacientePorId(String(pid)); if (p) report.paciente = p; } catch (e) { // ignore } } } } catch (e) { // ignore } return report; }; // When selectedRange changes (and isn't custom), compute start/end dates useEffect(() => { const now = new Date(); if (selectedRange === 'todos') { setStartDate(null); setEndDate(null); return; } if (selectedRange === 'semana') { const start = new Date(now); start.setDate(now.getDate() - now.getDay()); // sunday const end = new Date(start); end.setDate(start.getDate() + 6); setStartDate(start.toISOString().slice(0,10)); setEndDate(end.toISOString().slice(0,10)); return; } if (selectedRange === 'mes') { const start = new Date(now.getFullYear(), now.getMonth(), 1); const end = new Date(now.getFullYear(), now.getMonth() + 1, 0); setStartDate(start.toISOString().slice(0,10)); setEndDate(end.toISOString().slice(0,10)); return; } // custom: leave startDate/endDate as-is }, [selectedRange]); const filteredLaudos = (laudos || []).filter(l => { // If a specific start/end date is set, use that range if (startDate && endDate) { const ds = getReportDate(l); if (!ds) return false; const d = new Date(ds); if (isNaN(d.getTime())) return false; const start = new Date(startDate + 'T00:00:00'); const end = new Date(endDate + 'T23:59:59'); return d >= start && d <= end; } // Fallback to selectedRange heuristics if (!selectedRange) return true; const ds = getReportDate(l); return isInRange(ds, selectedRange); }); function DateRangeButtons() { return ( <> ); } // SearchBox inserido aqui para acessar reports, setLaudos e loadReports function SearchBox() { const [searchTerm, setSearchTerm] = useState(''); const [searching, setSearching] = useState(false); const { token } = useAuth(); const isMaybeId = (s: string) => { const t = s.trim(); if (!t) return false; if (t.includes('-') && t.length > 10) return true; if (t.toUpperCase().startsWith('REL-')) return true; const digits = t.replace(/\D/g, ''); if (digits.length >= 8) return true; return false; }; const doSearch = async () => { const term = searchTerm.trim(); if (!term) return; setSearching(true); try { if (isMaybeId(term)) { try { let r: any = null; // Try direct API lookup first try { r = await buscarRelatorioPorId(term); } catch (e) { console.warn('[SearchBox] buscarRelatorioPorId failed, will try loadReportById fallback', e); } // Fallback: use hook loader if direct API didn't return if (!r) { try { r = await loadReportById(term); } catch (e) { console.warn('[SearchBox] loadReportById fallback failed', e); } } if (r) { // If token exists, attempt batch enrichment like useReports const enriched: any = { ...r }; // Collect possible patient/doctor ids from payload const pidCandidates: string[] = []; const didCandidates: string[] = []; const pid = (r as any).patient_id ?? (r as any).patient ?? (r as any).paciente ?? null; if (pid) pidCandidates.push(String(pid)); const possiblePatientName = (r as any).patient_name ?? (r as any).patient_full_name ?? (r as any).paciente?.full_name ?? (r as any).paciente?.nome ?? null; if (possiblePatientName) { enriched.paciente = enriched.paciente ?? {}; enriched.paciente.full_name = possiblePatientName; } const did = (r as any).requested_by ?? (r as any).created_by ?? (r as any).executante ?? null; if (did) didCandidates.push(String(did)); // If token available, perform batch fetch to get full patient/doctor objects if (token) { try { if (pidCandidates.length) { const patients = await buscarPacientesPorIds(pidCandidates); if (patients && patients.length) { const p = patients[0]; enriched.paciente = enriched.paciente ?? {}; enriched.paciente.full_name = enriched.paciente.full_name || p.full_name || (p as any).nome; enriched.paciente.id = enriched.paciente.id || p.id; enriched.paciente.cpf = enriched.paciente.cpf || p.cpf; } } if (didCandidates.length) { const doctors = await buscarMedicosPorIds(didCandidates); if (doctors && doctors.length) { const d = doctors[0]; enriched.executante = enriched.executante || d.full_name || (d as any).nome; } } } catch (e) { // fallback: continue with payload-only enrichment console.warn('[SearchBox] batch enrichment failed, falling back to payload-only enrichment', e); } } // Final payload-only fallbacks (ensure id/cpf/order_number are populated) const possiblePatientId = (r as any).paciente?.id ?? (r as any).patient?.id ?? (r as any).patient_id ?? (r as any).patientId ?? (r as any).id ?? undefined; if (possiblePatientId && !enriched.paciente?.id) { enriched.paciente = enriched.paciente ?? {}; enriched.paciente.id = possiblePatientId; } const possibleCpf = (r as any).patient_cpf ?? (r as any).paciente?.cpf ?? (r as any).patient?.cpf ?? null; if (possibleCpf) { enriched.paciente = enriched.paciente ?? {}; enriched.paciente.cpf = possibleCpf; } const execName = (r as any).requested_by_name ?? (r as any).requester_name ?? (r as any).requestedByName ?? (r as any).executante_name ?? (r as any).created_by_name ?? (r as any).createdByName ?? (r as any).executante ?? (r as any).requested_by ?? (r as any).created_by ?? ''; if (execName) enriched.executante = enriched.executante || execName; if ((r as any).order_number) enriched.order_number = (r as any).order_number; setLaudos([enriched]); return; } } catch (err: any) { console.warn('Relatório não encontrado por ID:', err); } } const lower = term.toLowerCase(); const filtered = (reports || []).filter((x: any) => { const name = (x.paciente?.full_name || x.patient_name || x.patient_full_name || x.order_number || x.exame || x.exam || '').toString().toLowerCase(); return name.includes(lower); }); if (filtered.length) setLaudos(filtered); else setLaudos([]); } finally { setSearching(false); } }; const handleKey = (e: React.KeyboardEvent) => { if (e.key === 'Enter') doSearch(); }; const handleClear = async () => { setSearchTerm(''); try { // Reuse the same logic as initial load so Clear restores the doctor's assigned laudos await loadAssignedLaudos(); } catch (err) { console.warn('[SearchBox] erro ao restaurar laudos do médico ao limpar busca:', err); // Safe fallback to whatever reports are available setLaudos(reports || []); } }; return (
setSearchTerm(e.target.value)} onKeyDown={handleKey} />
); } // helper to load laudos for the patients assigned to the logged-in user const loadAssignedLaudos = async () => { try { const assignments = await import('@/lib/assignment').then(m => m.listAssignmentsForUser(user?.id || '')); const patientIds = Array.isArray(assignments) ? assignments.map(a => String(a.patient_id)).filter(Boolean) : []; if (patientIds.length === 0) { setLaudos([]); return; } try { const reportsMod = await import('@/lib/reports'); if (typeof reportsMod.listarRelatoriosPorPacientes === 'function') { const batch = await reportsMod.listarRelatoriosPorPacientes(patientIds); const mineOnly = (batch || []).filter((r: any) => { const requester = ((r.requested_by ?? r.created_by ?? r.executante ?? r.requestedBy ?? r.createdBy) || '').toString(); return user?.id && requester && requester === user.id; }); const enriched = await (async (reportsArr: any[]) => { if (!reportsArr || !reportsArr.length) return reportsArr; const pids = reportsArr.map(r => String(getReportPatientId(r))).filter(Boolean); if (!pids.length) return reportsArr; try { const patients = await buscarPacientesPorIds(pids); const map = new Map((patients || []).map((p: any) => [String(p.id), p])); return reportsArr.map((r: any) => { const pid = String(getReportPatientId(r)); return { ...r, paciente: r.paciente ?? map.get(pid) ?? r.paciente } as any; }); } catch (e) { return reportsArr; } })(mineOnly); setLaudos(enriched || []); return; } else { const allReports: any[] = []; for (const pid of patientIds) { try { const rels = await import('@/lib/reports').then(m => m.listarRelatoriosPorPaciente(pid)); if (Array.isArray(rels) && rels.length) { const mine = rels.filter((r: any) => { const requester = ((r.requested_by ?? r.created_by ?? r.executante ?? r.requestedBy ?? r.createdBy) || '').toString(); return user?.id && requester && requester === user.id; }); if (mine.length) allReports.push(...mine); } } catch (err) { console.warn('[LaudoManager] falha ao carregar relatórios para paciente', pid, err); } } const enrichedAll = await (async (reportsArr: any[]) => { if (!reportsArr || !reportsArr.length) return reportsArr; const pids = reportsArr.map(r => String(getReportPatientId(r))).filter(Boolean); if (!pids.length) return reportsArr; try { const patients = await buscarPacientesPorIds(pids); const map = new Map((patients || []).map((p: any) => [String(p.id), p])); return reportsArr.map((r: any) => ({ ...r, paciente: r.paciente ?? map.get(String(getReportPatientId(r))) ?? r.paciente } as any)); } catch (e) { return reportsArr; } })(allReports); setLaudos(enrichedAll); return; } } catch (err) { console.warn('[LaudoManager] erro ao carregar relatórios em batch, tentando por paciente individual', err); const allReports: any[] = []; for (const pid of patientIds) { try { const rels = await import('@/lib/reports').then(m => m.listarRelatoriosPorPaciente(pid)); if (Array.isArray(rels) && rels.length) { const mine = rels.filter((r: any) => { const requester = ((r.requested_by ?? r.created_by ?? r.executante ?? r.requestedBy ?? r.createdBy) || '').toString(); return user?.id && requester && requester === user.id; }); if (mine.length) allReports.push(...mine); } } catch (e) { console.warn('[LaudoManager] falha ao carregar relatórios para paciente', pid, e); } } const enrichedAll = await (async (reportsArr: any[]) => { if (!reportsArr || !reportsArr.length) return reportsArr; const pids = reportsArr.map(r => String(getReportPatientId(r))).filter(Boolean); if (!pids.length) return reportsArr; try { const patients = await buscarPacientesPorIds(pids); const map = new Map((patients || []).map((p: any) => [String(p.id), p])); return reportsArr.map((r: any) => ({ ...r, paciente: r.paciente ?? map.get(String(getReportPatientId(r))) ?? r.paciente } as any)); } catch (e) { return reportsArr; } })(allReports); setLaudos(enrichedAll); return; } } catch (e) { console.warn('[LaudoManager] erro ao carregar laudos para pacientes atribuídos:', e); setLaudos(reports || []); } }; // carregar laudos ao montar - somente dos pacientes atribuídos ao médico logado useEffect(() => { let mounted = true; (async () => { // call the helper and bail if the component unmounted during async work await loadAssignedLaudos(); })(); return () => { mounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // sincroniza quando reports mudarem no hook (fallback) useEffect(() => { if (!laudos || laudos.length === 0) setLaudos(reports || []); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Sort reports newest-first (more recent dates at the top) const sortedLaudos = React.useMemo(() => { const arr = (filteredLaudos || []).slice(); arr.sort((a: any, b: any) => { try { const da = new Date(getReportDate(a) || 0).getTime() || 0; const db = new Date(getReportDate(b) || 0).getTime() || 0; return db - da; } catch (e) { return 0; } }); return arr; }, [filteredLaudos]); const [activeTab, setActiveTab] = useState("descobrir"); const [laudoSelecionado, setLaudoSelecionado] = useState(null); const [isViewing, setIsViewing] = useState(false); const [isCreatingNew, setIsCreatingNew] = useState(false); return (
{/* Header */}

Gerenciamento de Laudo

Nesta seção você pode gerenciar todos os laudos gerados.

{/* Tabs */}
{/* Filtros */}
{/* Search input integrado com busca por ID */}
{ setStartDate(e.target.value); setSelectedRange('custom'); }} className="p-1 text-sm h-10" /> - { setEndDate(e.target.value); setSelectedRange('custom'); }} className="p-1 text-sm h-10" />
{/* date range buttons: Semana / Mês */}
{/* Filtros e pesquisa removidos por solicitação */}
{/* Tabela para desktop e cards empilháveis para mobile */}
{/* Desktop / tablet (md+) - tabela com scroll horizontal */}
Pedido Data Prazo Paciente Executante/Solicitante Exame/Classificação Ação {sortedLaudos.map((laudo, idx) => (
{laudo.urgente && (
)} {getReportPatientName(laudo) || laudo.order_number || getShortId(laudo.id)}
{formatReportDate(getReportDate(laudo))}
{laudo?.hora || new Date(laudo?.data || laudo?.created_at || laudo?.due_at || Date.now()).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
{(laudo?.prazo ?? laudo?.due_at) ? formatReportDate(laudo?.due_at ?? laudo?.prazo) : '-'}
{ (() => { // prefer explicit fields const explicit = laudo?.prazo_hora ?? laudo?.due_time ?? laudo?.hora ?? null; if (explicit) return explicit; // fallback: try to parse due_at / prazo datetime and extract time const due = laudo?.due_at ?? laudo?.prazo ?? laudo?.dueDate ?? laudo?.data ?? null; if (!due) return '-'; try { const d = new Date(due); if (isNaN(d.getTime())) return '-'; return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch (e) { return '-'; } })() }
{getReportPatientName(laudo) || '—'}
{getReportPatientCpf(laudo) ? `CPF: ${getReportPatientCpf(laudo)}` : ''}
{ (() => { const possibleName = laudo.requested_by_name ?? laudo.requester_name ?? laudo.requestedByName ?? laudo.executante_name ?? laudo.executante ?? laudo.executante_name ?? laudo.executante; if (possibleName && typeof possibleName === 'string' && possibleName.trim().length) return possibleName; const possibleId = (laudo.requested_by ?? laudo.created_by ?? laudo.executante ?? laudo.requestedBy ?? laudo.createdBy) || ''; if (possibleId && user?.id && possibleId === user.id) return (profileData as any)?.nome || user?.name || possibleId; return possibleName || possibleId || '-'; })() } {getReportExam(laudo) || "-"}
))}
{/* Mobile - cards empilháveis */}
{sortedLaudos.map((laudo, idx) => (
{getReportExam(laudo) || '-'}
{formatReportDate(getReportDate(laudo))} {laudo?.hora ? `• ${laudo.hora}` : ''}
{getReportPatientName(laudo) ? getShortId(laudo.id) : ''}
{getReportPatientName(laudo) || '—'}
{getReportPatientCpf(laudo) ? `CPF: ${getReportPatientCpf(laudo)}` : ''}
{ (() => { const possibleName = laudo.requested_by_name ?? laudo.requester_name ?? laudo.requestedByName ?? laudo.executante_name ?? laudo.executante ?? laudo.executante_name ?? laudo.executante; if (possibleName && typeof possibleName === 'string' && possibleName.trim().length) return possibleName; const possibleId = (laudo.requested_by ?? laudo.created_by ?? laudo.executante ?? laudo.requestedBy ?? laudo.createdBy) || ''; if (possibleId && user?.id && possibleId === user.id) return (profileData as any)?.nome || user?.name || possibleId; return possibleName || possibleId || '-'; })() }
))}
{/* Visualizador de Laudo */} {isViewing && laudoSelecionado && ( setIsViewing(false)} /> )} {/* Editor para Novo Laudo */} {isCreatingNew && ( setIsCreatingNew(false)} isNewLaudo={true} createNewReport={createNewReport} updateExistingReport={updateExistingReport} reloadReports={loadReports} onSaved={async (r:any) => { try { // If report has an id, fetch full report and open viewer if (r && (r.id || r.order_number)) { const id = r.id ?? r.order_number; const full = await loadReportById(String(id)); await ensurePaciente(full); // prepend to laudos list so it appears immediately setLaudos(prev => [full, ...(prev || [])]); setLaudoSelecionado(full); setIsViewing(true); } else { setLaudoSelecionado(r); setIsViewing(true); } // refresh global reports list too try { await loadReports(); } catch {} } catch (e) { // fallback: open what we have setLaudoSelecionado(r); setIsViewing(true); } }} /> )} {/* Editor para Paciente Específico */} {isEditingForPatient && selectedPatientForLaudo && ( {})} isNewLaudo={!selectedPatientForLaudo?.id} preSelectedPatient={selectedPatientForLaudo.paciente || selectedPatientForLaudo} createNewReport={createNewReport} updateExistingReport={updateExistingReport} reloadReports={loadReports} onSaved={async (r:any) => { try { if (r && (r.id || r.order_number)) { const id = r.id ?? r.order_number; const full = await loadReportById(String(id)); await ensurePaciente(full); setLaudos(prev => [full, ...(prev || [])]); setLaudoSelecionado(full); setIsViewing(true); } else { setLaudoSelecionado(r); setIsViewing(true); } try { await loadReports(); } catch {} } catch (e) { setLaudoSelecionado(r); setIsViewing(true); } }} /> )}
); } // Visualizador de Laudo (somente leitura) function LaudoViewer({ laudo, onClose }: { laudo: any; onClose: () => void }) { return (
{/* Header */}

Visualizar Laudo

Paciente: {getPatientName(laudo?.paciente) || getPatientName(laudo) || '—'} | CPF: {getReportPatientCpf(laudo) ?? laudo?.patient_cpf ?? '-'} | {laudo?.especialidade ?? laudo?.exame ?? '-'}

{/* Content */}
{/* Header do Laudo */}

LAUDO MÉDICO - {(laudo.especialidade ?? laudo.exame ?? '').toString().toUpperCase()}

Data: {formatReportDate(getReportDate(laudo))}

{/* Dados do Paciente */}

Dados do Paciente:

Nome: {getPatientName(laudo?.paciente) || getPatientName(laudo) || '-'}

CPF: {getPatientCpf(laudo?.paciente) ?? laudo?.patient_cpf ?? '-'}

Idade: {getPatientAge(laudo?.paciente) ? `${getPatientAge(laudo?.paciente)} anos` : (getPatientAge(laudo) ? `${getPatientAge(laudo)} anos` : '-')}

Sexo: {getPatientSex(laudo?.paciente) ?? getPatientSex(laudo) ?? '-'}

CID: {laudo?.cid ?? laudo?.cid_code ?? '-'}

{/* Conteúdo do Laudo */}
') }} />
{/* Exame */} {((laudo.exame ?? laudo.exam ?? laudo.especialidade ?? laudo.report_type) || '').toString().length > 0 && (

Exame / Especialidade:

{laudo.exame ?? laudo.exam ?? laudo.especialidade ?? laudo.report_type}

)} {/* Diagnóstico */} {((laudo.diagnostico ?? laudo.diagnosis) || '').toString().length > 0 && (

Diagnóstico:

{laudo.diagnostico ?? laudo.diagnosis}

)} {/* Conclusão */} {((laudo.conclusao ?? laudo.conclusion) || '').toString().length > 0 && (

Conclusão:

{laudo.conclusao ?? laudo.conclusion}

)} {/* Diagnóstico e Conclusão */} {laudo.diagnostico && (

Diagnóstico:

{laudo.diagnostico}

)} {laudo.conclusao && (

Conclusão:

{laudo.conclusao}

)} {/* Assinatura */}
{(() => { const signatureName = laudo?.created_by_name ?? laudo?.createdByName ?? ((laudo?.created_by && user?.id && laudo.created_by === user.id) ? profileData.nome : (laudo?.created_by_name || profileData.nome)); return ( <>

{signatureName}

{profileData.crm ? `CRM: ${String(profileData.crm).replace(/^(?:CRM\s*)+/i, '').trim()}` : 'CRM não informado'}{laudo.especialidade ? ` - ${laudo.especialidade}` : ''}

Data: {formatReportDate(getReportDate(laudo))}

); })()}
{/* Footer */}
); } // Editor de Laudo Avançado (para novos laudos) function LaudoEditor({ pacientes, laudo, onClose, isNewLaudo, preSelectedPatient, createNewReport, updateExistingReport, reloadReports, onSaved }: { pacientes?: any[]; laudo?: any; onClose: () => void; isNewLaudo?: boolean; preSelectedPatient?: any; createNewReport?: (data: any) => Promise; updateExistingReport?: (id: string, data: any) => Promise; reloadReports?: () => Promise; onSaved?: (r:any) => void }) { const { toast } = useToast(); const [activeTab, setActiveTab] = useState("editor"); const [content, setContent] = useState(laudo?.conteudo || ""); const [showPreview, setShowPreview] = useState(false); const [pacienteSelecionado, setPacienteSelecionado] = useState(preSelectedPatient || null); const [listaPacientes, setListaPacientes] = useState([]); // Novo: campos para solicitante e prazo // solicitanteId será enviado ao backend (sempre o id do usuário logado) const [solicitanteId, setSolicitanteId] = useState(user?.id || ""); // displaySolicitante é apenas para exibição (nome do usuário) e NÃO é enviado ao backend // Prefer profileData.nome (nome do médico carregado) — cai back para user.name ou email const displaySolicitante = ((profileData as any) && ((profileData as any).nome || (profileData as any).nome_social)) || user?.name || (user?.profile as any)?.full_name || user?.email || ''; const [prazoDate, setPrazoDate] = useState(""); const [prazoTime, setPrazoTime] = useState(""); // Pega token do usuário logado (passado explicitamente para listarPacientes) const { token } = useAuth(); // Carregar pacientes reais do Supabase ao abrir o modal ou quando o token mudar useEffect(() => { async function fetchPacientes() { try { if (!token) { setListaPacientes([]); return; } const pacientes = await listarPacientes(); setListaPacientes(pacientes || []); } catch (err) { console.warn('Erro ao carregar pacientes:', err); setListaPacientes([]); } } fetchPacientes(); }, [token]); const [campos, setCampos] = useState({ cid: laudo?.cid || "", diagnostico: laudo?.diagnostico || "", conclusao: laudo?.conclusao || "", exame: laudo?.exame || "", especialidade: laudo?.especialidade || "", mostrarData: true, mostrarAssinatura: true }); const [imagens, setImagens] = useState([]); const [templates] = useState([ "Exame normal, sem alterações significativas", "Paciente em acompanhamento ambulatorial", "Recomenda-se retorno em 30 dias", "Alterações compatíveis com processo inflamatório", "Resultado dentro dos parâmetros de normalidade", "Recomendo seguimento com especialista" ]); const sigCanvasRef = useRef(null); // Estado para imagem da assinatura const [assinaturaImg, setAssinaturaImg] = useState(null); useEffect(() => { if (!sigCanvasRef.current) return; const handleEnd = () => { const url = sigCanvasRef.current.getTrimmedCanvas().toDataURL('image/png'); setAssinaturaImg(url); }; const canvas = sigCanvasRef.current; if (canvas && canvas.canvas) { canvas.canvas.addEventListener('mouseup', handleEnd); canvas.canvas.addEventListener('touchend', handleEnd); } return () => { if (canvas && canvas.canvas) { canvas.canvas.removeEventListener('mouseup', handleEnd); canvas.canvas.removeEventListener('touchend', handleEnd); } }; }, [sigCanvasRef]); const handleClearSignature = () => { if (sigCanvasRef.current) { sigCanvasRef.current.clear(); } setAssinaturaImg(null); }; // Carregar dados do laudo existente quando disponível (mais robusto: suporta vários nomes de campo) useEffect(() => { if (laudo && !isNewLaudo) { // Conteúdo: aceita 'conteudo', 'content_html', 'contentHtml', 'content' const contentValue = laudo.conteudo ?? laudo.content_html ?? laudo.contentHtml ?? laudo.content ?? ""; setContent(contentValue); // Campos: use vários fallbacks const cidValue = laudo.cid ?? laudo.cid_code ?? ''; const diagnosticoValue = laudo.diagnostico ?? laudo.diagnosis ?? ''; const conclusaoValue = laudo.conclusao ?? laudo.conclusion ?? ''; const exameValue = laudo.exame ?? laudo.exam ?? laudo.especialidade ?? ''; const especialidadeValue = laudo.especialidade ?? laudo.exame ?? laudo.exam ?? ''; const mostrarDataValue = typeof laudo.hide_date === 'boolean' ? !laudo.hide_date : true; const mostrarAssinaturaValue = typeof laudo.hide_signature === 'boolean' ? !laudo.hide_signature : true; setCampos({ cid: cidValue, diagnostico: diagnosticoValue, conclusao: conclusaoValue, exame: exameValue, especialidade: especialidadeValue, mostrarData: mostrarDataValue, mostrarAssinatura: mostrarAssinaturaValue }); // Paciente: não sobrescrever se já existe preSelectedPatient ou pacienteSelecionado if (!pacienteSelecionado) { const pacienteFromLaudo = laudo.paciente ?? laudo.patient ?? null; if (pacienteFromLaudo) { setPacienteSelecionado(pacienteFromLaudo); } else if (laudo.patient_id && listaPacientes && listaPacientes.length) { const found = listaPacientes.find(p => String(p.id) === String(laudo.patient_id)); if (found) setPacienteSelecionado(found); } } // preencher solicitanteId/prazo quando existe laudo (edição) // preferimos manter o solicitanteId como o user id; se o laudo tiver requested_by que pareça um id, usamos ele const possibleRequestedById = laudo.requested_by ?? laudo.created_by ?? null; if (possibleRequestedById && typeof possibleRequestedById === 'string' && possibleRequestedById.length > 5) { setSolicitanteId(possibleRequestedById); } else { setSolicitanteId(user?.id || ""); } const dueRaw = laudo.due_at ?? laudo.prazo ?? laudo.dueDate ?? laudo.data ?? null; if (dueRaw) { try { const d = new Date(dueRaw); if (!isNaN(d.getTime())) { setPrazoDate(d.toISOString().slice(0,10)); setPrazoTime(d.toTimeString().slice(0,5)); } } catch (e) { // ignore invalid date } } // assinatura: aceitar vários campos possíveis const sig = laudo.assinaturaImg ?? laudo.signature_image ?? laudo.signature ?? laudo.sign_image ?? null; if (sig) setAssinaturaImg(sig); } }, [laudo, isNewLaudo, pacienteSelecionado, listaPacientes]); // Histórico para desfazer/refazer const [history, setHistory] = useState([]); const [historyIndex, setHistoryIndex] = useState(-1); // Atualiza histórico ao digitar useEffect(() => { if (history[historyIndex] !== content) { const newHistory = history.slice(0, historyIndex + 1); setHistory([...newHistory, content]); setHistoryIndex(newHistory.length); } // eslint-disable-next-line }, [content]); const handleUndo = () => { if (historyIndex > 0) { setContent(history[historyIndex - 1]); setHistoryIndex(historyIndex - 1); } }; const handleRedo = () => { if (historyIndex < history.length - 1) { setContent(history[historyIndex + 1]); setHistoryIndex(historyIndex + 1); } }; // Formatação avançada const formatText = (type: string, value?: any) => { const textarea = document.querySelector('textarea') as HTMLTextAreaElement; if (!textarea) return; const start = textarea.selectionStart; const end = textarea.selectionEnd; const selectedText = textarea.value.substring(start, end); let formattedText = ""; switch(type) { case "bold": formattedText = selectedText ? `**${selectedText}**` : "**texto em negrito**"; break; case "italic": formattedText = selectedText ? `*${selectedText}*` : "*texto em itálico*"; break; case "underline": formattedText = selectedText ? `__${selectedText}__` : "__texto sublinhado__"; break; case "list-ul": formattedText = selectedText ? selectedText.split('\n').map(l => `• ${l}`).join('\n') : "• item da lista"; break; case "list-ol": formattedText = selectedText ? selectedText.split('\n').map((l,i) => `${i+1}. ${l}`).join('\n') : "1. item da lista"; break; case "indent": formattedText = selectedText ? selectedText.split('\n').map(l => ` ${l}`).join('\n') : " "; break; case "outdent": formattedText = selectedText ? selectedText.split('\n').map(l => l.replace(/^\s{1,4}/, "")).join('\n') : ""; break; case "align-left": formattedText = selectedText ? `[left]${selectedText}[/left]` : "[left]Texto à esquerda[/left]"; break; case "align-center": formattedText = selectedText ? `[center]${selectedText}[/center]` : "[center]Texto centralizado[/center]"; break; case "align-right": formattedText = selectedText ? `[right]${selectedText}[/right]` : "[right]Texto à direita[/right]"; break; case "align-justify": formattedText = selectedText ? `[justify]${selectedText}[/justify]` : "[justify]Texto justificado[/justify]"; break; case "font-size": formattedText = selectedText ? `[size=${value}]${selectedText}[/size]` : `[size=${value}]Texto tamanho ${value}[/size]`; break; case "font-family": formattedText = selectedText ? `[font=${value}]${selectedText}[/font]` : `[font=${value}]${value}[/font]`; break; case "font-color": formattedText = selectedText ? `[color=${value}]${selectedText}[/color]` : `[color=${value}]${value}[/color]`; break; default: return; } const newText = textarea.value.substring(0, start) + formattedText + textarea.value.substring(end); setContent(newText); }; const insertTemplate = (template: string) => { setContent((prev: string) => prev ? `${prev}\n\n${template}` : template); }; const handleImageUpload = (e: React.ChangeEvent) => { const files = Array.from(e.target.files || []); files.forEach(file => { const reader = new FileReader(); reader.onload = (e) => { setImagens(prev => [...prev, { id: Date.now() + Math.random(), name: file.name, url: e.target?.result, type: file.type }]); }; reader.readAsDataURL(file); }); }; const processContent = (content: string) => { return content .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/__(.*?)__/g, '$1') .replace(/\[left\]([\s\S]*?)\[\/left\]/g, '
$1
') .replace(/\[center\]([\s\S]*?)\[\/center\]/g, '
$1
') .replace(/\[right\]([\s\S]*?)\[\/right\]/g, '
$1
') .replace(/\[justify\]([\s\S]*?)\[\/justify\]/g, '
$1
') .replace(/\[size=(\d+)\]([\s\S]*?)\[\/size\]/g, '$2') .replace(/\[font=([^\]]+)\]([\s\S]*?)\[\/font\]/g, '$2') .replace(/\[color=([^\]]+)\]([\s\S]*?)\[\/color\]/g, '$2') .replace(/{{sexo_paciente}}/g, pacienteSelecionado?.sexo || laudo?.paciente?.sexo || '[SEXO]') .replace(/{{diagnostico}}/g, campos.diagnostico || '[DIAGNÓSTICO]') .replace(/{{conclusao}}/g, campos.conclusao || '[CONCLUSÃO]') .replace(/\n/g, '
'); }; return (
{/* Header */}

{isNewLaudo ? "Novo Laudo Médico" : "Editar Laudo Existente"}

{isNewLaudo ? (

Crie um novo laudo selecionando um paciente

) : (

Paciente: {getPatientName(pacienteSelecionado) || getPatientName(laudo?.paciente) || getPatientName(laudo) || '-'} | CPF: {getReportPatientCpf(laudo) ?? laudo?.patient_cpf ?? '-'} | {laudo?.especialidade}

)}
{/* Seleção de Paciente (apenas para novos laudos) */} {isNewLaudo && (
{!pacienteSelecionado ? (
) : (
{getPatientName(pacienteSelecionado)}
{getPatientCpf(pacienteSelecionado) ? `CPF: ${getPatientCpf(pacienteSelecionado)} | ` : ''} {pacienteSelecionado?.birth_date ? `Nascimento: ${pacienteSelecionado.birth_date}` : (getPatientAge(pacienteSelecionado) ? `Idade: ${getPatientAge(pacienteSelecionado)} anos` : '')} {getPatientSex(pacienteSelecionado) ? ` | Sexo: ${getPatientSex(pacienteSelecionado)}` : ''}
{!preSelectedPatient && ( )}
)} {/* Novos campos: Solicitante e Prazo */}
{/* Mostrar o nome do usuário logado de forma estática (não editável) */}
setPrazoDate(e.target.value)} /> setPrazoTime(e.target.value)} />

Defina a data e hora do prazo (opcional).

)}
{/* Tabs */}
{/* Informações tab removed - only Editor/Imagens/Campos/Pré-visualização remain */}
{/* Content */}
{/* Left Panel */}
{/* 'Informações' section removed to keep editor-only experience */} {activeTab === "editor" && (
{/* Toolbar */}
{/* Tamanho da fonte */} formatText('font-size', e.target.value)} className="w-14 border rounded px-1 py-0.5 text-xs mr-2" title="Tamanho da fonte" /> {/* Família da fonte */} {/* Cor da fonte */} formatText('font-color', e.target.value)} className="w-6 h-6 border rounded mr-2" title="Cor da fonte" /> {/* Alinhamento */} {/* Listas */} {/* Recuo */} {/* Desfazer/Refazer */}
{templates.map((template, idx) => ( ))}
{/* Editor */}