diff --git a/susconecta/app/(main-routes)/calendar/page.tsx b/susconecta/app/(main-routes)/calendar/page.tsx index c4afdea..d2349f5 100644 --- a/susconecta/app/(main-routes)/calendar/page.tsx +++ b/susconecta/app/(main-routes)/calendar/page.tsx @@ -3,25 +3,18 @@ // Imports mantidos import { useEffect, useState } from "react"; import dynamic from "next/dynamic"; -import Link from "next/link"; // --- Imports do EventManager (NOVO) - MANTIDOS --- import { EventManager, type Event } from "@/components/features/general/event-manager"; import { v4 as uuidv4 } from 'uuid'; // Usado para IDs de fallback // Imports mantidos -import { Sidebar } from "@/components/layout/sidebar"; -import { PagesHeader } from "@/components/features/dashboard/header"; import { Button } from "@/components/ui/button"; +import { useAuth } from "@/hooks/useAuth"; import { mockWaitingList } from "@/lib/mocks/appointment-mocks"; import "./index.css"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; import { ThreeDWallCalendar, CalendarEvent } from "@/components/ui/three-dwall-calendar"; // Calendário 3D mantido +import { PatientRegistrationForm } from "@/components/features/forms/patient-registration-form"; const ListaEspera = dynamic( () => import("@/components/features/agendamento/ListaEspera"), @@ -29,28 +22,38 @@ const ListaEspera = dynamic( ); export default function AgendamentoPage() { + const { user, token } = useAuth(); const [appointments, setAppointments] = useState([]); - const [waitingList, setWaitingList] = useState(mockWaitingList); - const [activeTab, setActiveTab] = useState<"calendar" | "espera" | "3d">("calendar"); - + const [activeTab, setActiveTab] = useState<"calendar" | "3d">("calendar"); const [threeDEvents, setThreeDEvents] = useState([]); + // Padroniza idioma da página para pt-BR (afeta componentes que usam o lang do documento) + useEffect(() => { + try { + // Atributos no + document.documentElement.lang = "pt-BR"; + document.documentElement.setAttribute("xml:lang", "pt-BR"); + document.documentElement.setAttribute("data-lang", "pt-BR"); + // Cookie de locale (usado por apps com i18n) + const oneYear = 60 * 60 * 24 * 365; + document.cookie = `NEXT_LOCALE=pt-BR; Path=/; Max-Age=${oneYear}; SameSite=Lax`; + } catch { + // ignore + } + }, []); + // --- NOVO ESTADO --- // Estado para alimentar o NOVO EventManager com dados da API const [managerEvents, setManagerEvents] = useState([]); const [managerLoading, setManagerLoading] = useState(true); + + // Estado para o formulário de registro de paciente + const [showPatientForm, setShowPatientForm] = useState(false); useEffect(() => { document.addEventListener("keydown", (event) => { - if (event.key === "c") { - setActiveTab("calendar"); - } - if (event.key === "f") { - setActiveTab("espera"); - } - if (event.key === "3") { - setActiveTab("3d"); - } + if (event.key === "c") setActiveTab("calendar"); + if (event.key === "3") setActiveTab("3d"); }); }, []); @@ -86,18 +89,23 @@ export default function AgendamentoPage() { const patient = (patientsById[String(obj.patient_id)]?.full_name) || obj.patient_name || obj.patient_full_name || obj.patient || 'Paciente'; const title = `${patient}: ${obj.appointment_type ?? obj.type ?? ''}`.trim(); - - let color = "gray"; // Cor padrão - if (obj.status === 'confirmed') color = 'green'; - if (obj.status === 'pending') color = 'orange'; + + // Mapeamento de cores padronizado: + // azul = solicitado; verde = confirmado; laranja = pendente; vermelho = cancelado; azul como fallback + const status = String(obj.status || "").toLowerCase(); + let color: Event["color"] = "blue"; + if (status === "confirmed" || status === "confirmado") color = "green"; + else if (status === "pending" || status === "pendente") color = "orange"; + else if (status === "canceled" || status === "cancelado" || status === "cancelled") color = "red"; + else if (status === "requested" || status === "solicitado") color = "blue"; return { - id: obj.id || uuidv4(), // Usa ID da API ou gera um - title: title, - description: `Agendamento para ${patient}. Status: ${obj.status || 'N/A'}.`, + id: obj.id || uuidv4(), + title, + description: `Agendamento para ${patient}. Status: ${obj.status || 'N/A'}.`, startTime: start, endTime: end, - color: color, + color, }; }); setManagerEvents(newManagerEvents); @@ -146,10 +154,6 @@ export default function AgendamentoPage() { } }; - const handleNotifyPatient = (patientId: string) => { - console.log(`Notificando paciente ${patientId}`); - }; - const handleAddEvent = (event: CalendarEvent) => { setThreeDEvents((prev) => [...prev, event]); }; @@ -172,26 +176,10 @@ export default function AgendamentoPage() { Navegue através dos atalhos: Calendário (C), Fila de espera (F) ou 3D (3).

-
- - - Opções » - - - - Agendamento - - - Procedimento - - - Financeiro - - - - +
+
+
+
- + {/* Legenda de status (estilo Google Calendar) */} +
+
+
+ + Solicitado +
+
+ + Confirmado
@@ -242,17 +237,22 @@ export default function AgendamentoPage() { events={threeDEvents} onAddEvent={handleAddEvent} onRemoveEvent={handleRemoveEvent} + onOpenAddPatientForm={() => setShowPatientForm(true)} /> - ) : ( - // A Lista de Espera foi MANTIDA - {}} - /> - )} + ) : null} + + {/* Formulário de Registro de Paciente */} + { + console.log('[Calendar] Novo paciente registrado:', newPaciente); + setShowPatientForm(false); + }} + /> ); diff --git a/susconecta/app/globals.css b/susconecta/app/globals.css index df339bd..d41795d 100644 --- a/susconecta/app/globals.css +++ b/susconecta/app/globals.css @@ -123,3 +123,47 @@ @apply bg-background text-foreground font-sans; } } + +/* Esconder botões com ícones de lixo */ +button:has(.lucide-trash2), +button:has(.lucide-trash), +button[class*="trash"] { + display: none !important; +} + +/* Esconder campos de input embaixo do calendário 3D */ +input[placeholder="Nome do paciente"], +input[placeholder^="dd/mm"], +input[type="date"][value=""] { + display: none !important; +} + +/* Esconder botão "Adicionar Paciente" */ +/* Removido seletor vazio - será tratado por outros seletores */ + +/* Afastar X do popup (dialog-close) para longe das setas */ +[data-slot="dialog-close"], +button[aria-label="Close"], +.fc button[aria-label*="Close"] { + right: 16px !important; + top: 8px !important; + position: absolute !important; +} + +/* Esconder footer/header extras do calendário que mostram os campos */ +.fc .fc-toolbar input, +.fc .fc-toolbar [type="date"], +.fc .fc-toolbar [placeholder*="paciente"] { + display: none !important; +} + +/* Esconder row com campos de pesquisa - estrutura mantida pelo calendário */ + +/* Esconder botões de trash/delete em todos os popups */ +[role="dialog"] button[class*="hover:text-destructive"], +[role="dialog"] button[aria-label*="delete"], +[role="dialog"] button[aria-label*="excluir"], +[role="dialog"] button[aria-label*="remove"] { + display: none !important; +} + diff --git a/susconecta/app/paciente/page.tsx b/susconecta/app/paciente/page.tsx index 697c02f..a6e8ccb 100644 --- a/susconecta/app/paciente/page.tsx +++ b/susconecta/app/paciente/page.tsx @@ -18,7 +18,8 @@ import Link from 'next/link' import ProtectedRoute from '@/components/shared/ProtectedRoute' import { useAuth } from '@/hooks/useAuth' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById } from '@/lib/api' +import { buscarPacientes, buscarPacientePorUserId, getUserInfo, listarAgendamentos, buscarMedicosPorIds, buscarMedicos, atualizarPaciente, buscarPacientePorId, getDoctorById, atualizarAgendamento, deletarAgendamento } from '@/lib/api' +import { CalendarRegistrationForm } from '@/components/features/forms/calendar-registration-form' import { buscarRelatorioPorId, listarRelatoriosPorMedico } from '@/lib/reports' import { ENV_CONFIG } from '@/lib/env-config' import { listarRelatoriosPorPaciente } from '@/lib/reports' @@ -35,7 +36,6 @@ const strings = { ultimosExames: 'Últimos Exames', mensagensNaoLidas: 'Mensagens Não Lidas', agendar: 'Agendar', - reagendar: 'Reagendar', cancelar: 'Cancelar', detalhes: 'Detalhes', adicionarCalendario: 'Adicionar ao calendário', @@ -445,11 +445,10 @@ export default function PacientePage() { - + ) } - // Consultas fictícias const [currentDate, setCurrentDate] = useState(new Date()) // helper: produce a local YYYY-MM-DD key (uses local timezone, not toISOString UTC) @@ -519,10 +518,15 @@ export default function PacientePage() { const selectedDate = new Date(currentDate); selectedDate.setHours(0, 0, 0, 0); const isSelectedDateToday = selectedDate.getTime() === today.getTime() - // Appointments state (loaded when component mounts) - const [appointments, setAppointments] = useState(null) - const [loadingAppointments, setLoadingAppointments] = useState(false) - const [appointmentsError, setAppointmentsError] = useState(null) + // Appointments state (loaded when component mounts) + const [appointments, setAppointments] = useState(null) + const [doctorsMap, setDoctorsMap] = useState>({}) // Store doctor info by ID + const [loadingAppointments, setLoadingAppointments] = useState(false) + const [appointmentsError, setAppointmentsError] = useState(null) + // expanded appointment id for inline details (kept for possible fallback) + const [expandedId, setExpandedId] = useState(null) + // selected appointment for modal details + const [selectedAppointment, setSelectedAppointment] = useState(null) useEffect(() => { let mounted = true @@ -608,6 +612,7 @@ export default function PacientePage() { } }) + setDoctorsMap(doctorsMap) setAppointments(mapped) } catch (err: any) { console.warn('[Consultas] falha ao carregar agendamentos', err) @@ -638,6 +643,60 @@ export default function PacientePage() { const _dialogSource = (appointments !== null ? appointments : consultasFicticias) const _todaysAppointments = (_dialogSource || []).filter((c: any) => c.data === todayStr) + // helper: present a localized label for appointment status + const statusLabel = (s: any) => { + const raw = (s === null || s === undefined) ? '' : String(s) + const key = raw.toLowerCase() + const map: Record = { + 'requested': 'Solicitado', + 'request': 'Solicitado', + 'confirmed': 'Confirmado', + 'confirmada': 'Confirmada', + 'confirmado': 'Confirmado', + 'completed': 'Concluído', + 'concluído': 'Concluído', + 'cancelled': 'Cancelado', + 'cancelada': 'Cancelada', + 'cancelado': 'Cancelado', + 'pending': 'Pendente', + 'pendente': 'Pendente', + 'checked_in': 'Registrado', + 'in_progress': 'Em andamento', + 'no_show': 'Não compareceu' + } + return map[key] || raw + } + + // map an appointment (row) to the CalendarRegistrationForm's formData shape + const mapAppointmentToFormData = (appointment: any) => { + // Use the raw appointment with all fields: doctor_id, scheduled_at, appointment_type, etc. + const schedIso = appointment.scheduled_at || (appointment.data && appointment.hora ? `${appointment.data}T${appointment.hora}` : null) || null + const baseDate = schedIso ? new Date(schedIso) : new Date() + const appointmentDate = schedIso ? baseDate.toISOString().split('T')[0] : '' + const startTime = schedIso ? baseDate.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : (appointment.hora || '') + const duration = appointment.duration_minutes ?? appointment.duration ?? 30 + + // Get doctor name from doctorsMap if available + const docName = appointment.medico || (appointment.doctor_id ? doctorsMap[String(appointment.doctor_id)]?.full_name : null) || appointment.doctor_name || appointment.professional_name || '---' + + return { + id: appointment.id, + patientName: docName, + patientId: null, + doctorId: appointment.doctor_id ?? null, + professionalName: docName, + appointmentDate, + startTime, + endTime: '', + status: appointment.status || undefined, + appointmentType: appointment.appointment_type || appointment.type || (appointment.local ? 'presencial' : 'teleconsulta'), + duration_minutes: duration, + notes: appointment.notes || '', + } + } + + + return (
{/* Hero Section */} @@ -771,7 +830,7 @@ export default function PacientePage() { ? 'bg-linear-to-r from-amber-500 to-amber-600 shadow-amber-500/20' : 'bg-linear-to-r from-red-500 to-red-600 shadow-red-500/20' }`}> - {consulta.status} + {statusLabel(consulta.status)}
@@ -781,28 +840,43 @@ export default function PacientePage() { type="button" size="sm" className="border border-primary/30 text-primary bg-primary/5 hover:bg-primary! hover:text-white! hover:border-primary! transition-all duration-200 focus-visible:ring-2 focus-visible:ring-primary/40 active:scale-95 text-xs font-semibold flex-1" + onClick={() => setSelectedAppointment(consulta)} > Detalhes - {consulta.status !== 'Cancelada' && ( - - )} + {/* Reagendar removed by request */} {consulta.status !== 'Cancelada' && ( )} + + {/* Inline detalhes removed: modal will show details instead */} + )) @@ -811,6 +885,45 @@ export default function PacientePage() { + + + + !open && setSelectedAppointment(null)}> + + + Detalhes da Consulta + Detalhes da consulta +
+ {selectedAppointment ? ( + <> +
+
Profissional: {selectedAppointment.medico || '-'}
+
Especialidade: {selectedAppointment.especialidade || '-'}
+
+ +
+
Data: {(function(d:any,h:any){ try{ const dt = new Date(String(d) + 'T' + String(h||'00:00')); return formatDatePt(dt) }catch(e){ return String(d||'-') } })(selectedAppointment.data, selectedAppointment.hora)}
+
Hora: {selectedAppointment.hora || '-'}
+
Status: {statusLabel(selectedAppointment.status) || '-'}
+
+ + ) : ( +
Carregando...
+ )} +
+
+ +
+ +
+
+
+
+ + {/* Reagendar feature removed */} + ) } @@ -1262,7 +1375,7 @@ export default function PacientePage() { setReportsPage(1) }, [reports]) - return ( + return (<>

Laudos

@@ -1334,10 +1447,13 @@ export default function PacientePage() { )} +
+ + !open && setSelectedReport(null)}> - - + + {selectedReport && ( (() => { const looksLikeIdStr = (s: any) => { @@ -1422,102 +1538,199 @@ export default function PacientePage() { - + ) } function Perfil() { - const hasAddress = Boolean(profileData.endereco || profileData.cidade || profileData.cep) return ( -
+
+ {/* Header com Título e Botão */}
-

Meu Perfil

+
+

Meu Perfil

+

Bem-vindo à sua área exclusiva.

+
{!isEditingProfile ? ( - ) : ( -
- - +
)}
-
- {/* Informações Pessoais */} -
-

Informações Pessoais

-
- -

{profileData.nome}

- Este campo não pode ser alterado + + {/* Grid de 3 colunas (2 + 1) */} +
+ {/* Coluna Esquerda - Informações Pessoais */} +
+ {/* Informações Pessoais */} +
+

Informações Pessoais

+ +
+ {/* Nome Completo */} +
+ +
+ {profileData.nome || "Não preenchido"} +
+

+ Este campo não pode ser alterado +

+
+ + {/* Email */} +
+ +
+ {profileData.email || "Não preenchido"} +
+

+ Este campo não pode ser alterado +

+
+ + {/* Telefone */} +
+ + {isEditingProfile ? ( + handleProfileChange('telefone', e.target.value)} + className="mt-2" + placeholder="(00) 00000-0000" + maxLength={15} + /> + ) : ( +
+ {profileData.telefone || "Não preenchido"} +
+ )} +
+
-
- - {isEditingProfile ? ( - handleProfileChange('email', e.target.value)} /> - ) : ( -

{profileData.email}

- )} + + {/* Endereço e Contato */} +
+

Endereço e Contato

+ +
+ {/* Logradouro */} +
+ + {isEditingProfile ? ( + handleProfileChange('endereco', e.target.value)} + className="mt-2" + placeholder="Rua, avenida, etc." + /> + ) : ( +
+ {profileData.endereco || "Não preenchido"} +
+ )} +
+ + {/* Cidade */} +
+ + {isEditingProfile ? ( + handleProfileChange('cidade', e.target.value)} + className="mt-2" + placeholder="São Paulo" + /> + ) : ( +
+ {profileData.cidade || "Não preenchido"} +
+ )} +
+ + {/* CEP */} +
+ + {isEditingProfile ? ( + handleProfileChange('cep', e.target.value)} + className="mt-2" + placeholder="00000-000" + /> + ) : ( +
+ {profileData.cep || "Não preenchido"} +
+ )} +
+
-
- +
+ + {/* Coluna Direita - Foto do Perfil */} +
+
+

Foto do Perfil

+ {isEditingProfile ? ( - handleProfileChange('telefone', e.target.value)} /> +
+ handleProfileChange('foto_url', newUrl)} + userName={profileData.nome} + /> +
) : ( -

{profileData.telefone}

+
+ + + {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'} + + + +
+

+ {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'PC'} +

+
+
)}
- {/* Endereço e Contato (render apenas se existir algum dado) */} - {hasAddress && ( -
-

Endereço

-
- - {isEditingProfile ? ( - handleProfileChange('endereco', e.target.value)} /> - ) : ( -

{profileData.endereco}

- )} -
-
- - {isEditingProfile ? ( - handleProfileChange('cidade', e.target.value)} /> - ) : ( -

{profileData.cidade}

- )} -
-
- - {isEditingProfile ? ( - handleProfileChange('cep', e.target.value)} /> - ) : ( -

{profileData.cep}

- )} -
- {/* Biografia removed: not used */} -
- )} -
- {/* Foto do Perfil */} -
-

Foto do Perfil

- handleProfileChange('foto_url', newUrl)} - userName={profileData.nome} - />
) diff --git a/susconecta/app/paciente/resultados/ResultadosClient.tsx b/susconecta/app/paciente/resultados/ResultadosClient.tsx index 1fa8519..b471740 100644 --- a/susconecta/app/paciente/resultados/ResultadosClient.tsx +++ b/susconecta/app/paciente/resultados/ResultadosClient.tsx @@ -148,7 +148,7 @@ export default function ResultadosClient() { try { setLoadingMedicos(true) console.log('[ResultadosClient] Initial doctors fetch starting') - const list = await buscarMedicos('medico').catch((err) => { + const list = await buscarMedicos('').catch((err) => { console.error('[ResultadosClient] Initial fetch error:', err) return [] }) @@ -175,7 +175,7 @@ export default function ResultadosClient() { setAgendaByDoctor({}) setAgendasExpandida({}) // termo de busca: usar a especialidade escolhida - const termo = (especialidadeHero && especialidadeHero !== 'Veja mais') ? especialidadeHero : 'medico' + const termo = (especialidadeHero && especialidadeHero !== 'Veja mais') ? especialidadeHero : '' console.log('[ResultadosClient] Fetching doctors with term:', termo) const list = await buscarMedicos(termo).catch((err) => { console.error('[ResultadosClient] buscarMedicos error:', err) @@ -219,9 +219,9 @@ export default function ResultadosClient() { }, [searchQuery]) // 3) Carregar horários disponíveis para um médico (próximos 7 dias) e agrupar por dia - async function loadAgenda(doctorId: string) { - if (!doctorId) return - if (agendaLoading[doctorId]) return + async function loadAgenda(doctorId: string): Promise<{ iso: string; label: string } | null> { + if (!doctorId) return null + if (agendaLoading[doctorId]) return null setAgendaLoading((s) => ({ ...s, [doctorId]: true })) try { // janela de 7 dias @@ -271,10 +271,12 @@ export default function ResultadosClient() { nearest = { iso: s.iso, label: s.label } } - setAgendaByDoctor((prev) => ({ ...prev, [doctorId]: days })) - setNearestSlotByDoctor((prev) => ({ ...prev, [doctorId]: nearest })) + setAgendaByDoctor((prev) => ({ ...prev, [doctorId]: days })) + setNearestSlotByDoctor((prev) => ({ ...prev, [doctorId]: nearest })) + return nearest } catch (e: any) { showToast('error', e?.message || 'Falha ao buscar horários') + return null } finally { setAgendaLoading((s) => ({ ...s, [doctorId]: false })) } @@ -752,19 +754,7 @@ export default function ResultadosClient() { - - - {/* Search input para buscar médico por nome */} + {/* Search input para buscar médico por nome (movido antes do Select de bairro para ficar ao lado visualmente) */}
+ + diff --git a/susconecta/app/profissional/page.tsx b/susconecta/app/profissional/page.tsx index 485fc90..17e9860 100644 --- a/susconecta/app/profissional/page.tsx +++ b/susconecta/app/profissional/page.tsx @@ -25,7 +25,7 @@ import { } 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 { User, FolderOpen, X, Users, MessageSquare, ClipboardList, Plus, Edit, 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, @@ -41,6 +41,7 @@ import dayGridPlugin from "@fullcalendar/daygrid"; import timeGridPlugin from "@fullcalendar/timegrid"; import interactionPlugin from "@fullcalendar/interaction"; import ptBrLocale from "@fullcalendar/core/locales/pt-br"; +import { PatientRegistrationForm } from "@/components/features/forms/patient-registration-form"; const FullCalendar = dynamic(() => import("@fullcalendar/react"), { ssr: false, @@ -230,7 +231,7 @@ const ProfissionalPage = () => { })(); return () => { mounted = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [user?.email, user?.id]); @@ -378,6 +379,9 @@ const ProfissionalPage = () => { const [editingEvent, setEditingEvent] = useState(null); const [showPopup, setShowPopup] = useState(false); const [showActionModal, setShowActionModal] = useState(false); + const [showDayModal, setShowDayModal] = useState(false); + const [selectedDayDate, setSelectedDayDate] = useState(null); + const [showPatientForm, setShowPatientForm] = useState(false); const [step, setStep] = useState(1); const [newEvent, setNewEvent] = useState({ title: "", @@ -686,6 +690,13 @@ const ProfissionalPage = () => {

Agenda do Dia

+
{/* Navegação de Data */} @@ -718,7 +729,7 @@ const ProfissionalPage = () => {
{/* Lista de Pacientes do Dia */} -
+
{todayEvents.length === 0 ? (
@@ -2656,150 +2667,216 @@ const ProfissionalPage = () => { const renderPerfilSection = () => ( -
+
+ {/* Header com Título e Botão */}
-

Meu Perfil

+
+

Meu Perfil

+

Bem-vindo à sua área exclusiva.

+
{!isEditingProfile ? ( - ) : (
- -
)}
-
- {/* Informações Pessoais */} -
-

Informações Pessoais

- -
- -

{profileData.nome}

- Este campo não pode ser alterado -
+ {/* Grid de 3 colunas (2 + 1) */} +
+ {/* Coluna Esquerda - Informações Pessoais */} +
+ {/* Informações Pessoais */} +
+

Informações Pessoais

-
- - {isEditingProfile ? ( - handleProfileChange('email', e.target.value)} - /> - ) : ( -

{profileData.email}

- )} -
+
+ {/* Nome Completo */} +
+ +
+ {profileData.nome || "Não preenchido"} +
+

+ Este campo não pode ser alterado +

+
-
- - {isEditingProfile ? ( - handleProfileChange('telefone', e.target.value)} - /> - ) : ( -

{profileData.telefone}

- )} -
+ {/* Email */} +
+ + {isEditingProfile ? ( + handleProfileChange('email', e.target.value)} + className="mt-2" + type="email" + /> + ) : ( +
+ {profileData.email || "Não preenchido"} +
+ )} +
-
- -

{profileData.crm}

- Este campo não pode ser alterado -
+ {/* Telefone */} +
+ + {isEditingProfile ? ( + handleProfileChange('telefone', e.target.value)} + className="mt-2" + placeholder="(00) 00000-0000" + /> + ) : ( +
+ {profileData.telefone || "Não preenchido"} +
+ )} +
-
- - {isEditingProfile ? ( - handleProfileChange('especialidade', e.target.value)} - /> - ) : ( -

{profileData.especialidade}

- )} -
-
+ {/* CRM */} +
+ +
+ {profileData.crm || "Não preenchido"} +
+

+ Este campo não pode ser alterado +

+
- {/* Endereço e Contato */} -
-

Endereço e Contato

- -
- - {isEditingProfile ? ( - handleProfileChange('endereco', e.target.value)} - /> - ) : ( -

{profileData.endereco}

- )} -
- -
- - {isEditingProfile ? ( - handleProfileChange('cidade', e.target.value)} - /> - ) : ( -

{profileData.cidade}

- )} -
- -
- - {isEditingProfile ? ( - handleProfileChange('cep', e.target.value)} - /> - ) : ( -

{profileData.cep}

- )} -
- - {/* Biografia removida: não é um campo no registro de médico */} -
-
- - {/* Foto do Perfil */} -
-

Foto do Perfil

-
- - - {profileData.nome.split(' ').map(n => n[0]).join('').toUpperCase()} - - - {isEditingProfile && ( -
- -

- Formatos aceitos: JPG, PNG (máx. 2MB) -

+ {/* Especialidade */} +
+ + {isEditingProfile ? ( + handleProfileChange('especialidade', e.target.value)} + className="mt-2" + placeholder="Ex: Cardiologia" + /> + ) : ( +
+ {profileData.especialidade || "Não preenchido"} +
+ )} +
- )} +
+ + {/* Endereço e Contato */} +
+

Endereço e Contato

+ +
+ {/* Logradouro */} +
+ + {isEditingProfile ? ( + handleProfileChange('endereco', e.target.value)} + className="mt-2" + placeholder="Rua, avenida, etc." + /> + ) : ( +
+ {profileData.endereco || "Não preenchido"} +
+ )} +
+ + {/* Cidade */} +
+ + {isEditingProfile ? ( + handleProfileChange('cidade', e.target.value)} + className="mt-2" + placeholder="São Paulo" + /> + ) : ( +
+ {profileData.cidade || "Não preenchido"} +
+ )} +
+ + {/* CEP */} +
+ + {isEditingProfile ? ( + handleProfileChange('cep', e.target.value)} + className="mt-2" + placeholder="00000-000" + /> + ) : ( +
+ {profileData.cep || "Não preenchido"} +
+ )} +
+
+
+
+ + {/* Coluna Direita - Foto do Perfil */} +
+
+

Foto do Perfil

+ +
+ + + {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'MD'} + + + +
+

+ {profileData.nome?.split(' ').map((n: string) => n[0]).join('').toUpperCase().slice(0, 2) || 'MD'} +

+
+
+
@@ -2838,8 +2915,8 @@ const ProfissionalPage = () => { ); case 'laudos': return renderLaudosSection(); - case 'comunicacao': - return renderComunicacaoSection(); + // case 'comunicacao': + // return renderComunicacaoSection(); case 'perfil': return renderPerfilSection(); default: @@ -2910,14 +2987,15 @@ const ProfissionalPage = () => { Laudos - + */} -
)} + + {/* Modal para visualizar pacientes de um dia específico */} + {showDayModal && selectedDayDate && ( +
+
+ {/* Header com navegação */} +
+ + +

+ {selectedDayDate.toLocaleDateString('pt-BR', { + weekday: 'long', + day: 'numeric', + month: 'long', + year: 'numeric' + })} +

+ + + +
+ + +
+ + {/* Content */} +
+ {(() => { + const dayStr = selectedDayDate.toISOString().split('T')[0]; + const dayEvents = events.filter(e => e.date === dayStr).sort((a, b) => a.time.localeCompare(b.time)); + + if (dayEvents.length === 0) { + return ( +
+ +

Nenhuma consulta agendada para este dia

+
+ ); + } + + return ( +
+

+ {dayEvents.length} consulta{dayEvents.length !== 1 ? 's' : ''} agendada{dayEvents.length !== 1 ? 's' : ''} +

+ {dayEvents.map((appointment) => { + const paciente = pacientes.find(p => p.nome === appointment.title); + return ( +
+
+
+

+ + {appointment.title} +

+ + {appointment.type} + +
+
+ + + {appointment.time} + + {paciente && ( + + CPF: {getPatientCpf(paciente)} • {getPatientAge(paciente)} anos + + )} +
+
+
+ ); + })} +
+ ); + })()} +
+
+
+ )} + + {/* Formulário para cadastro de paciente */} + { + // Adicionar o novo paciente à lista e recarregar + setPacientes((prev) => [...prev, newPaciente]); + }} + />
); diff --git a/susconecta/components/event-manager.tsx b/susconecta/components/event-manager.tsx new file mode 100644 index 0000000..1a19417 --- /dev/null +++ b/susconecta/components/event-manager.tsx @@ -0,0 +1,1485 @@ +"use client" + +import React, { useState, useCallback, useMemo } from "react" +import { Button } from "@/components/ui/button" +import { Card } from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { Badge } from "@/components/ui/badge" +import { ChevronLeft, ChevronRight, Plus, Calendar, Clock, Grid3x3, List, Search, Filter, X } from "lucide-react" +import { cn } from "@/lib/utils" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuCheckboxItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export interface Event { + id: string + title: string + description?: string + startTime: Date + endTime: Date + color: string + category?: string + attendees?: string[] + tags?: string[] +} + +export interface EventManagerProps { + events?: Event[] + onEventCreate?: (event: Omit) => void + onEventUpdate?: (id: string, event: Partial) => void + onEventDelete?: (id: string) => void + categories?: string[] + colors?: { name: string; value: string; bg: string; text: string }[] + defaultView?: "month" | "week" | "day" | "list" + className?: string + availableTags?: string[] +} + +const defaultColors = [ + { name: "Blue", value: "blue", bg: "bg-blue-500", text: "text-blue-700" }, + { name: "Green", value: "green", bg: "bg-green-500", text: "text-green-700" }, + { name: "Purple", value: "purple", bg: "bg-purple-500", text: "text-purple-700" }, + { name: "Orange", value: "orange", bg: "bg-orange-500", text: "text-orange-700" }, + { name: "Pink", value: "pink", bg: "bg-pink-500", text: "text-pink-700" }, + { name: "Red", value: "red", bg: "bg-red-500", text: "text-red-700" }, +] + +export function EventManager({ + events: initialEvents = [], + onEventCreate, + onEventUpdate, + onEventDelete, + categories = ["Meeting", "Task", "Reminder", "Personal"], + colors = defaultColors, + defaultView = "month", + className, + availableTags = ["Important", "Urgent", "Work", "Personal", "Team", "Client"], +}: EventManagerProps) { + const [events, setEvents] = useState(initialEvents) + const [currentDate, setCurrentDate] = useState(new Date()) + const [view, setView] = useState<"month" | "week" | "day" | "list">(defaultView) + const [selectedEvent, setSelectedEvent] = useState(null) + const [isDialogOpen, setIsDialogOpen] = useState(false) + const [isCreating, setIsCreating] = useState(false) + const [draggedEvent, setDraggedEvent] = useState(null) + const [newEvent, setNewEvent] = useState>({ + title: "", + description: "", + color: colors[0].value, + category: categories[0], + tags: [], + }) + + const [searchQuery, setSearchQuery] = useState("") + const [selectedColors, setSelectedColors] = useState([]) + const [selectedTags, setSelectedTags] = useState([]) + const [selectedCategories, setSelectedCategories] = useState([]) + + const filteredEvents = useMemo(() => { + return events.filter((event) => { + // Search filter + if (searchQuery) { + const query = searchQuery.toLowerCase() + const matchesSearch = + event.title.toLowerCase().includes(query) || + event.description?.toLowerCase().includes(query) || + event.category?.toLowerCase().includes(query) || + event.tags?.some((tag) => tag.toLowerCase().includes(query)) + + if (!matchesSearch) return false + } + + // Color filter + if (selectedColors.length > 0 && !selectedColors.includes(event.color)) { + return false + } + + // Tag filter + if (selectedTags.length > 0) { + const hasMatchingTag = event.tags?.some((tag) => selectedTags.includes(tag)) + if (!hasMatchingTag) return false + } + + // Category filter + if (selectedCategories.length > 0 && event.category && !selectedCategories.includes(event.category)) { + return false + } + + return true + }) + }, [events, searchQuery, selectedColors, selectedTags, selectedCategories]) + + const hasActiveFilters = selectedColors.length > 0 || selectedTags.length > 0 || selectedCategories.length > 0 + + const clearFilters = () => { + setSelectedColors([]) + setSelectedTags([]) + setSelectedCategories([]) + setSearchQuery("") + } + + const handleCreateEvent = useCallback(() => { + if (!newEvent.title || !newEvent.startTime || !newEvent.endTime) return + + const event: Event = { + id: Math.random().toString(36).substr(2, 9), + title: newEvent.title, + description: newEvent.description, + startTime: newEvent.startTime, + endTime: newEvent.endTime, + color: newEvent.color || colors[0].value, + category: newEvent.category, + attendees: newEvent.attendees, + tags: newEvent.tags || [], + } + + setEvents((prev) => [...prev, event]) + onEventCreate?.(event) + setIsDialogOpen(false) + setIsCreating(false) + setNewEvent({ + title: "", + description: "", + color: colors[0].value, + category: categories[0], + tags: [], + }) + }, [newEvent, colors, categories, onEventCreate]) + + const handleUpdateEvent = useCallback(() => { + if (!selectedEvent) return + + setEvents((prev) => prev.map((e) => (e.id === selectedEvent.id ? selectedEvent : e))) + onEventUpdate?.(selectedEvent.id, selectedEvent) + setIsDialogOpen(false) + setSelectedEvent(null) + }, [selectedEvent, onEventUpdate]) + + const handleDeleteEvent = useCallback( + (id: string) => { + setEvents((prev) => prev.filter((e) => e.id !== id)) + onEventDelete?.(id) + setIsDialogOpen(false) + setSelectedEvent(null) + }, + [onEventDelete], + ) + + const handleDragStart = useCallback((event: Event) => { + setDraggedEvent(event) + }, []) + + const handleDragEnd = useCallback(() => { + setDraggedEvent(null) + }, []) + + const handleDrop = useCallback( + (date: Date, hour?: number) => { + if (!draggedEvent) return + + const duration = draggedEvent.endTime.getTime() - draggedEvent.startTime.getTime() + const newStartTime = new Date(date) + if (hour !== undefined) { + newStartTime.setHours(hour, 0, 0, 0) + } + const newEndTime = new Date(newStartTime.getTime() + duration) + + const updatedEvent = { + ...draggedEvent, + startTime: newStartTime, + endTime: newEndTime, + } + + setEvents((prev) => prev.map((e) => (e.id === draggedEvent.id ? updatedEvent : e))) + onEventUpdate?.(draggedEvent.id, updatedEvent) + setDraggedEvent(null) + }, + [draggedEvent, onEventUpdate], + ) + + const navigateDate = useCallback( + (direction: "prev" | "next") => { + setCurrentDate((prev) => { + const newDate = new Date(prev) + if (view === "month") { + newDate.setMonth(prev.getMonth() + (direction === "next" ? 1 : -1)) + } else if (view === "week") { + newDate.setDate(prev.getDate() + (direction === "next" ? 7 : -7)) + } else if (view === "day") { + newDate.setDate(prev.getDate() + (direction === "next" ? 1 : -1)) + } + return newDate + }) + }, + [view], + ) + + const getColorClasses = useCallback( + (colorValue: string) => { + const color = colors.find((c) => c.value === colorValue) + return color || colors[0] + }, + [colors], + ) + + const toggleTag = (tag: string, isCreating: boolean) => { + if (isCreating) { + setNewEvent((prev) => ({ + ...prev, + tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag], + })) + } else { + setSelectedEvent((prev) => + prev + ? { + ...prev, + tags: prev.tags?.includes(tag) ? prev.tags.filter((t) => t !== tag) : [...(prev.tags || []), tag], + } + : null, + ) + } + } + + return ( +
+ {/* Header */} +
+
+

+ {view === "month" && + currentDate.toLocaleDateString("pt-BR", { + month: "long", + year: "numeric", + })} + {view === "week" && + `Semana de ${currentDate.toLocaleDateString("pt-BR", { + month: "short", + day: "numeric", + })}`} + {view === "day" && + currentDate.toLocaleDateString("pt-BR", { + weekday: "long", + month: "long", + day: "numeric", + year: "numeric", + })} + {view === "list" && "Todos os eventos"} +

+
+ + + +
+
+ +
+ {/* Mobile: Select dropdown */} +
+ +
+ + {/* Desktop: Button group */} +
+ + + + +
+ + +
+
+ +
+
+ + setSearchQuery(e.target.value)} + className="pl-9" + /> + {searchQuery && ( + + )} +
+ + {/* Mobile: Horizontal scroll with full-length buttons */} +
+
+ {/* Color Filter */} + + + + + + Filtrar por Cor + + {colors.map((color) => ( + { + setSelectedColors((prev) => + checked ? [...prev, color.value] : prev.filter((c) => c !== color.value), + ) + }} + > +
+
+ {color.name} +
+ + ))} + + + + {/* Tag Filter */} + + + + + + Filtrar por Tag + + {availableTags.map((tag) => ( + { + setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag))) + }} + > + {tag} + + ))} + + + + {/* Category Filter */} + + + + + + Filtrar por Categoria + + {categories.map((category) => ( + { + setSelectedCategories((prev) => + checked ? [...prev, category] : prev.filter((c) => c !== category), + ) + }} + > + {category} + + ))} + + + + {hasActiveFilters && ( + + )} +
+
+ + {/* Desktop: Original layout */} +
+ {/* Color Filter */} + + + + + + Filtrar por Cor + + {colors.map((color) => ( + { + setSelectedColors((prev) => + checked ? [...prev, color.value] : prev.filter((c) => c !== color.value), + ) + }} + > +
+
+ {color.name} +
+ + ))} + + + + {/* Tag Filter */} + + + + + + Filtrar por Tag + + {availableTags.map((tag) => ( + { + setSelectedTags((prev) => (checked ? [...prev, tag] : prev.filter((t) => t !== tag))) + }} + > + {tag} + + ))} + + + + {/* Category Filter */} + + + + + + Filtrar por Categoria + + {categories.map((category) => ( + { + setSelectedCategories((prev) => + checked ? [...prev, category] : prev.filter((c) => c !== category), + ) + }} + > + {category} + + ))} + + + + {hasActiveFilters && ( + + )} +
+
+ + {hasActiveFilters && ( +
+ Filtros ativos: + {selectedColors.map((colorValue) => { + const color = getColorClasses(colorValue) + return ( + +
+ {color.name} + + + ) + })} + {selectedTags.map((tag) => ( + + {tag} + + + ))} + {selectedCategories.map((category) => ( + + {category} + + + ))} +
+ )} + + {/* Calendar Views - Pass filteredEvents instead of events */} + {view === "month" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + onDragStart={(event) => handleDragStart(event)} + onDragEnd={() => handleDragEnd()} + onDrop={handleDrop} + getColorClasses={getColorClasses} + /> + )} + + {view === "week" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + onDragStart={(event) => handleDragStart(event)} + onDragEnd={() => handleDragEnd()} + onDrop={handleDrop} + getColorClasses={getColorClasses} + /> + )} + + {view === "day" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + onDragStart={(event) => handleDragStart(event)} + onDragEnd={() => handleDragEnd()} + onDrop={handleDrop} + getColorClasses={getColorClasses} + /> + )} + + {view === "list" && ( + { + setSelectedEvent(event) + setIsDialogOpen(true) + }} + getColorClasses={getColorClasses} + /> + )} + + {/* Event Dialog */} + + + + {isCreating ? "Criar Evento" : "Detalhes do Evento"} + + {isCreating ? "Adicione um novo evento ao seu calendário" : "Visualizar e editar detalhes do evento"} + + + +
+
+ + + isCreating + ? setNewEvent((prev) => ({ ...prev, title: e.target.value })) + : setSelectedEvent((prev) => (prev ? { ...prev, title: e.target.value } : null)) + } + placeholder="Título do evento" + /> +
+ +
+ +