feat(agenda+ui): responsividade mobile da agenda

- PageHeader empilha no mobile, titulo menor, acoes com flex-wrap
- AgendaView: toolbar de acoes com wrap; botao NOVO vira so icone no mobile
- FullCalendar: view de DIA no mobile (timeGridDay), toolbar enxuta + footer com Dia/Mes, altura por viewport (100dvh), CSS responsivo da toolbar/celulas, nowIndicator

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Builder
2026-06-09 22:12:55 +02:00
parent 619e88b490
commit 3db0fe0d9a
2 changed files with 31 additions and 9 deletions
+3 -3
View File
@@ -9,12 +9,12 @@ interface PageHeaderProps {
export const PageHeader: React.FC<PageHeaderProps> = ({ title, description, children }) => {
return (
<div className="flex justify-between items-center mb-6">
<div className="flex flex-col gap-3 sm:flex-row sm:justify-between sm:items-center mb-4 sm:mb-6">
<div>
<h2 className="text-2xl font-bold text-gray-900 uppercase">{title}</h2>
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 uppercase">{title}</h2>
{description && <p className="text-gray-500 text-xs uppercase font-bold mt-1">{description}</p>}
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 sm:gap-3 flex-wrap">
{children}
</div>
</div>
+28 -6
View File
@@ -369,6 +369,12 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void }> = ({
const loadAtividade = useCallback(async () => {
try { setAtividade(await HybridBackend.getAtividadeAgenda()); } catch { /* ignore */ }
}, []);
const [isMobile, setIsMobile] = useState(typeof window !== 'undefined' && window.innerWidth < 640);
useEffect(() => {
const onResize = () => setIsMobile(window.innerWidth < 640);
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, []);
const [importando, setImportando] = useState(false);
const handleImportarGoogle = async () => {
if (!confirm('Importar os agendamentos do Google (última semana + próximos) para o sistema? Eles passam a ser editáveis aqui.')) return;
@@ -553,7 +559,7 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void }> = ({
return (
<div className="space-y-6">
<PageHeader title="AGENDA CLÍNICA">
<div className="flex gap-4 items-center">
<div className="flex gap-2 sm:gap-3 items-center flex-wrap justify-end">
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="dentists-legend" direction="horizontal">
{(provided) => (
@@ -616,17 +622,21 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void }> = ({
</button>
)}
{(currentRole !== 'paciente') && (
<button onClick={() => { setReagendarPatient(null); setSelectedAppointment(null); setSelectedDateInfo({ dateStr: new Date().toISOString() }); setIsModalOpen(true); }} className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-bold uppercase flex items-center gap-2 hover:bg-blue-700 shadow-sm transition-colors">
<Plus size={16} /> NOVO AGENDAMENTO
<button onClick={() => { setReagendarPatient(null); setSelectedAppointment(null); setSelectedDateInfo({ dateStr: new Date().toISOString() }); setIsModalOpen(true); }} title="Novo agendamento" className="bg-blue-600 text-white px-3 sm:px-4 py-2 rounded-lg text-sm font-bold uppercase flex items-center gap-2 hover:bg-blue-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-4 relative" style={{ height: '750px' }}>
<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]">
<FullCalendar
key={isMobile ? 'mobile' : 'desktop'}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView="timeGridWeek"
headerToolbar={{ left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay' }}
initialView={isMobile ? 'timeGridDay' : 'timeGridWeek'}
headerToolbar={isMobile
? { left: 'prev,next', center: 'title', right: 'today' }
: { left: 'prev,next today', center: 'title', right: 'dayGridMonth,timeGridWeek,timeGridDay' }}
footerToolbar={isMobile ? { center: 'timeGridDay,dayGridMonth' } : undefined}
locale="pt-br"
buttonText={{ today: 'HOJE', month: 'MÊS', week: 'SEMANA', day: 'DIA' }}
slotMinTime="08:00:00"
@@ -639,6 +649,7 @@ export const AgendaView: React.FC<{ onNavigate?: (view: string) => void }> = ({
dateClick={handleDateClick}
eventClick={handleEventClick}
height="100%"
nowIndicator={true}
slotLabelFormat={{ hour: '2-digit', minute: '2-digit', hour12: false }}
eventContent={(eventInfo) => {
const isGoogle = eventInfo.event.extendedProps.isGoogle;
@@ -745,6 +756,17 @@ 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 — responsividade mobile */
@media (max-width: 639px) {
.fc .fc-toolbar.fc-header-toolbar { margin-bottom: .5rem; flex-wrap: wrap; gap: .4rem; }
.fc .fc-toolbar-title { font-size: .95rem; }
.fc .fc-button { padding: .25rem .55rem; font-size: .72rem; }
.fc .fc-toolbar-chunk { display: flex; align-items: center; }
.fc .fc-col-header-cell-cushion { font-size: .65rem; padding: 2px; }
.fc .fc-timegrid-slot-label-cushion, .fc .fc-timegrid-axis-cushion { font-size: .6rem; }
.fc .fc-timegrid-event .fc-event-main { padding: 1px 2px; }
.fc-footer-toolbar.fc-toolbar { margin-top: .5rem; }
}
`}</style>
<AppointmentModal
isOpen={isModalOpen}