feat(agenda): layout em 2 colunas no desktop + sidebar recolhível
- AgendaView: controles migrados do header horizontal para uma coluna lateral esquerda (AGENDAR, ícones de ação em faixa horizontal de tamanho igual, presença, legenda de dentistas); calendário em flex-1 ocupando a altura toda - Mobile revisto: cabeçalho compacto (título + AGENDAR) e calendário flex-1 - Sidebar do sistema recolhível no desktop (App: estado persistido + botão flutuante para reexibir); toggle fica ao lado do título "AGENDA CLÍNICA" - Toolbar do FullCalendar compactada (título/botões/cabeçalhos menores) - Título da agenda responsivo (não gera scroll horizontal na coluna estreita) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import FullCalendar from '@fullcalendar/react';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
import { Calendar as CalendarIcon, Plus, X, Loader2, GripVertical, Settings as GearIcon, Edit, Trash2, CalendarRange, Clock, User, Stethoscope, AlertCircle, CheckCircle2, Bell, CalendarClock, History, Download } from 'lucide-react';
|
||||
import { Calendar as CalendarIcon, Plus, X, Loader2, GripVertical, Settings as GearIcon, Edit, Trash2, CalendarRange, Clock, User, Stethoscope, AlertCircle, CheckCircle2, Bell, CalendarClock, History, Download, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
||||
import { AgendaSettingsModal } from '../components/AgendaSettingsModal.tsx';
|
||||
import { AgendaDetailModal, ScoreBadge, FamiliaChips } from '../components/AgendaDetailModal.tsx';
|
||||
import { WhatsChatDrawer } from './newwhats/WhatsChatDrawer.tsx';
|
||||
@@ -15,7 +15,6 @@ import { Dentista, Agendamento, Paciente, DentistaHorario } from '../types.ts';
|
||||
import { useGTOStore } from './LancarGTO.tsx';
|
||||
import { useHybridBackend } from '../hooks/useHybridBackend.ts';
|
||||
import { useToast } from '../contexts/ToastContext.tsx';
|
||||
import { PageHeader } from '../components/PageHeader.tsx';
|
||||
|
||||
// --- Reusable Debounce Hook ---
|
||||
function useDebounce(value: string, delay: number) {
|
||||
@@ -344,7 +343,7 @@ const AppointmentModal: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
export const AgendaView: React.FC<{ onNavigate?: (view: string) => void }> = ({ onNavigate }) => {
|
||||
export const AgendaView: React.FC<{ onNavigate?: (view: string) => void; sidebarCollapsed?: boolean; onToggleSidebar?: () => void }> = ({ onNavigate, sidebarCollapsed, onToggleSidebar }) => {
|
||||
const { data: agendamentos, refresh } = useHybridBackend<Agendamento[]>(HybridBackend.getAgendamentos);
|
||||
const { data: dentists, refresh: refreshDentists } = useHybridBackend<Dentista[]>(HybridBackend.getDentistas);
|
||||
const { data: googleEvents, refresh: refreshGoogle } = useHybridBackend<any[]>(HybridBackend.getGoogleEvents);
|
||||
@@ -600,13 +599,78 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void }> = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader title="AGENDA CLÍNICA">
|
||||
<div className="flex gap-2 sm:gap-3 items-center flex-wrap justify-end">
|
||||
<div className="flex flex-col lg:flex-row lg:gap-4 h-full">
|
||||
{/* Coluna de controles (desktop: lateral esquerda vertical; mobile: topo compacto) */}
|
||||
<aside className="shrink-0 mb-3 lg:mb-0 lg:w-56 lg:overflow-y-auto lg:pr-3 lg:border-r lg:border-gray-100">
|
||||
{/* Título + recolher menu (desktop, ao lado) + AGENDAR (mobile, à direita) */}
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
{onToggleSidebar && (
|
||||
<button onClick={onToggleSidebar} title={sidebarCollapsed ? 'Mostrar menu lateral' : 'Recolher menu lateral'}
|
||||
className="hidden lg:flex items-center justify-center w-8 h-8 shrink-0 rounded-lg text-gray-400 hover:text-gray-700 hover:bg-gray-100 transition-colors">
|
||||
{sidebarCollapsed ? <PanelLeftOpen size={18} /> : <PanelLeftClose size={18} />}
|
||||
</button>
|
||||
)}
|
||||
<h2 className="text-xl lg:text-base font-bold text-gray-900 uppercase flex-1 min-w-0 leading-tight break-words">AGENDA CLÍNICA</h2>
|
||||
{(currentRole !== 'paciente' && !souDentista) && (
|
||||
<button onClick={() => { setReagendarPatient(null); setSelectedAppointment(null); setSelectedDateInfo({ dateStr: new Date().toISOString() }); setIsModalOpen(true); }} title="Agendar"
|
||||
className="lg:hidden shrink-0 bg-teal-600 text-white px-3 py-2 rounded-lg text-xs font-bold uppercase flex items-center gap-1.5 hover:bg-teal-700 shadow-sm">
|
||||
<Plus size={16} /> AGENDAR
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2 sm:gap-3 items-center flex-wrap lg:flex-col lg:items-stretch">
|
||||
{/* AGENDAR (desktop, full-width no topo da coluna) */}
|
||||
{(currentRole !== 'paciente' && !souDentista) && (
|
||||
<button onClick={() => { setReagendarPatient(null); setSelectedAppointment(null); setSelectedDateInfo({ dateStr: new Date().toISOString() }); setIsModalOpen(true); }} title="Agendar"
|
||||
className="hidden lg:flex w-full bg-teal-600 text-white px-4 py-2.5 rounded-lg text-sm font-bold uppercase items-center justify-center gap-2 hover:bg-teal-700 shadow-sm transition-colors">
|
||||
<Plus size={18} /> AGENDAR
|
||||
</button>
|
||||
)}
|
||||
{/* ícones de ação — mesmo tamanho, distribuídos horizontalmente */}
|
||||
<div className="flex gap-2 w-full">
|
||||
{(currentRole !== 'paciente') && (
|
||||
<button onClick={() => { setShowAtividade(true); loadAtividade(); }} title="Atividade da equipe hoje"
|
||||
className="flex-1 flex items-center justify-center py-2.5 bg-white text-gray-400 hover:text-teal-600 border border-gray-200 rounded-xl transition-all hover:border-teal-200 hover:shadow-sm">
|
||||
<History size={20} />
|
||||
</button>
|
||||
)}
|
||||
{(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'donoconsultorio') && (
|
||||
<button onClick={handleImportarGoogle} disabled={importando} title="Importar agendamentos do Google para o sistema"
|
||||
className="flex-1 flex items-center justify-center py-2.5 bg-white text-gray-400 hover:text-emerald-600 border border-gray-200 rounded-xl transition-all hover:border-emerald-200 hover:shadow-sm disabled:opacity-50">
|
||||
{importando ? <Loader2 size={20} className="animate-spin" /> : <Download size={20} />}
|
||||
</button>
|
||||
)}
|
||||
{(currentRole === 'admin' || currentRole === 'donoclinica') && (
|
||||
<button onClick={() => setIsSettingsOpen(true)} title="Configurações da agenda"
|
||||
className="flex-1 flex items-center justify-center py-2.5 bg-white text-gray-400 hover:text-teal-600 border border-gray-200 rounded-xl transition-all hover:border-teal-200 hover:shadow-sm">
|
||||
<GearIcon size={20} />
|
||||
</button>
|
||||
)}
|
||||
{souDentista && (
|
||||
<button onClick={() => { setShowInteresses(true); loadInteresses(); }} title="Pedidos de interesse na sua agenda"
|
||||
className="relative flex-1 flex items-center justify-center py-2.5 bg-white text-gray-400 hover:text-teal-600 border border-gray-200 rounded-xl transition-all hover:border-teal-200 hover:shadow-sm">
|
||||
<Bell size={20} />
|
||||
{interessesAguardando > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-[9px] font-black rounded-full w-4 h-4 flex items-center justify-center">{interessesAguardando}</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{(currentRole !== 'paciente' && !souDentista) && (
|
||||
<button onClick={() => { setShowPendencias(true); loadPendencias(); }} title="Pacientes a reagendar"
|
||||
className="relative flex-1 flex items-center justify-center py-2.5 bg-white text-gray-400 hover:text-amber-600 border border-gray-200 rounded-xl transition-all hover:border-amber-200 hover:shadow-sm">
|
||||
<CalendarClock size={20} />
|
||||
{pendencias.length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-amber-500 text-white text-[9px] font-black rounded-full w-4 h-4 flex items-center justify-center">{pendencias.length}</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full"><AgendaPresence /></div>
|
||||
{/* Legenda de dentistas (desktop, no rodapé da coluna) */}
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="dentists-legend" direction="horizontal">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} className="hidden lg:flex gap-2 items-center">
|
||||
<div {...provided.droppableProps} ref={provided.innerRef} className="hidden lg:flex lg:flex-wrap gap-2 items-center lg:w-full lg:mt-1">
|
||||
{filteredDentists?.map((d, index) => (
|
||||
<Draggable key={d.id} draggableId={d.id} index={index} isDragDisabled={currentRole === 'dentista'}>
|
||||
{(provided, snapshot) => (
|
||||
@@ -628,50 +692,9 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void }> = ({
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<AgendaPresence />
|
||||
{(currentRole !== 'paciente') && (
|
||||
<button onClick={() => { setShowAtividade(true); loadAtividade(); }} title="Atividade da equipe hoje"
|
||||
className="p-2.5 bg-white text-gray-400 hover:text-teal-600 border border-gray-200 rounded-xl transition-all hover:border-teal-200 hover:shadow-sm">
|
||||
<History size={20} />
|
||||
</button>
|
||||
)}
|
||||
{(currentRole === 'admin' || currentRole === 'donoclinica' || currentRole === 'donoconsultorio') && (
|
||||
<button onClick={handleImportarGoogle} disabled={importando} title="Importar agendamentos do Google para o sistema"
|
||||
className="p-2.5 bg-white text-gray-400 hover:text-emerald-600 border border-gray-200 rounded-xl transition-all hover:border-emerald-200 hover:shadow-sm disabled:opacity-50">
|
||||
{importando ? <Loader2 size={20} className="animate-spin" /> : <Download size={20} />}
|
||||
</button>
|
||||
)}
|
||||
{(currentRole === 'admin' || currentRole === 'donoclinica') && (
|
||||
<button onClick={() => setIsSettingsOpen(true)} className="p-2.5 bg-white text-gray-400 hover:text-teal-600 border border-gray-200 rounded-xl transition-all hover:border-teal-200 hover:shadow-sm">
|
||||
<GearIcon size={20} />
|
||||
</button>
|
||||
)}
|
||||
{souDentista && (
|
||||
<button onClick={() => { setShowInteresses(true); loadInteresses(); }} title="Pedidos de interesse na sua agenda"
|
||||
className="relative p-2.5 bg-white text-gray-400 hover:text-teal-600 border border-gray-200 rounded-xl transition-all hover:border-teal-200 hover:shadow-sm">
|
||||
<Bell size={20} />
|
||||
{interessesAguardando > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-[9px] font-black rounded-full w-4 h-4 flex items-center justify-center">{interessesAguardando}</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{(currentRole !== 'paciente' && !souDentista) && (
|
||||
<button onClick={() => { setShowPendencias(true); loadPendencias(); }} title="Pacientes a reagendar"
|
||||
className="relative p-2.5 bg-white text-gray-400 hover:text-amber-600 border border-gray-200 rounded-xl transition-all hover:border-amber-200 hover:shadow-sm">
|
||||
<CalendarClock size={20} />
|
||||
{pendencias.length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-amber-500 text-white text-[9px] font-black rounded-full w-4 h-4 flex items-center justify-center">{pendencias.length}</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{(currentRole !== 'paciente' && !souDentista) && (
|
||||
<button onClick={() => { setReagendarPatient(null); setSelectedAppointment(null); setSelectedDateInfo({ dateStr: new Date().toISOString() }); setIsModalOpen(true); }} title="Novo agendamento" className="bg-teal-600 text-white px-3 sm:px-4 py-2 rounded-lg text-sm font-bold uppercase flex items-center gap-2 hover:bg-teal-700 shadow-sm transition-colors">
|
||||
<Plus size={18} /> <span className="hidden sm:inline">NOVO AGENDAMENTO</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-2 sm:p-4 relative h-[calc(100dvh-220px)] sm:h-[750px] min-h-[420px]">
|
||||
</aside>
|
||||
<div className="flex-1 min-w-0 bg-white rounded-xl shadow-sm border border-gray-200 p-2 sm:p-4 relative min-h-[420px] lg:min-h-0">
|
||||
<FullCalendar
|
||||
key={isMobile ? 'mobile' : 'desktop'}
|
||||
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
|
||||
@@ -810,6 +833,12 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void }> = ({
|
||||
.custom-scrollbar::-webkit-scrollbar { width: 4px; }
|
||||
.custom-scrollbar::-webkit-scrollbar-track { background: #f1f1f1; }
|
||||
.custom-scrollbar::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
|
||||
/* FullCalendar — toolbar/controles compactos (todas as telas) */
|
||||
.fc .fc-toolbar.fc-header-toolbar { margin-bottom: .6rem; }
|
||||
.fc .fc-toolbar-title { font-size: 1.05rem; font-weight: 700; }
|
||||
.fc .fc-button { padding: .3rem .65rem; font-size: .8rem; box-shadow: none !important; }
|
||||
.fc .fc-button-group { gap: 0; }
|
||||
.fc .fc-col-header-cell-cushion { font-size: .72rem; padding: 4px 2px; }
|
||||
/* FullCalendar — responsividade mobile */
|
||||
@media (max-width: 639px) {
|
||||
.fc .fc-toolbar.fc-header-toolbar { margin-bottom: .5rem; flex-wrap: wrap; gap: .4rem; }
|
||||
|
||||
Reference in New Issue
Block a user