// Tela: Calendário — semana atual + eventos do dia
const { useState: useStateCal } = React;
function NovoEventoModal({ onClose, onCreate, selectedDay }) {
const [titulo, setTitulo] = useStateCal('');
const [hora, setHora] = useStateCal('09:00');
const [dur, setDur] = useStateCal(60);
const [cor, setCor] = useStateCal('blue');
const toast = useToast();
const CORES = [
{ id: 'blue', label: 'Azul', bg: 'var(--acc-blue)' },
{ id: 'pink', label: 'Rosa', bg: 'var(--acc-pink)' },
{ id: 'yellow', label: 'Amarelo', bg: 'var(--acc-yellow)' },
{ id: 'mint', label: 'Verde', bg: 'var(--acc-mint)' },
{ id: 'lav', label: 'Lavanda', bg: 'var(--acc-lav)' },
];
const DURACOES = [30, 45, 60, 90, 120];
const valid = titulo.trim().length > 0;
const submit = () => {
if (!valid) return;
const novo = { hora, dur, titulo: titulo.trim(), color: cor, pessoas: [0] };
if (onCreate) onCreate(novo);
toast(`"${titulo.trim()}" agendado às ${hora} ✓`);
onClose();
};
return (
e.stopPropagation()}>
Novo Evento
{selectedDay} · adicione ao calendário
{ICN.x}
);
}
const HOURS = ['08:00','09:00','10:00','11:00','12:00','13:00','14:00','15:00','16:00','17:00','18:00'];
const DOWS = ['DOM','SEG','TER','QUA','QUI','SEX','SÁB'];
const PX_PER_MIN = 1; // 60px/hour
// May 2026 grid: starts on Friday (index 5)
const MAY_2026 = (() => {
const cells = [];
for (let i = 0; i < 5; i++) cells.push(null); // padding before May 1 (Fri = index 5, so 5 empty cells Sun-Thu)
for (let d = 1; d <= 31; d++) cells.push(d);
while (cells.length % 7 !== 0) cells.push(null);
const weeks = [];
for (let i = 0; i < cells.length; i += 7) weeks.push(cells.slice(i, i + 7));
return weeks;
})();
const EVENT_DAYS = { 11: EVENTOS_DIA, 13: EVENTOS_DIA, 15: EVENTOS_DIA.slice(0,3), 16: EVENTOS_DIA.slice(2) };
/* ── Modal de detalhe de evento ──────────────────────────── */
function EventoDetailModal({ evento, onClose, onDelete }) {
const { useState: useED } = React;
const [titulo, setTitulo] = useED(evento.titulo);
const toast = useToast();
const durLabel = evento.dur >= 60
? `${Math.floor(evento.dur/60)}h${evento.dur%60 ? ' ' + (evento.dur%60) + 'min' : ''}`
: `${evento.dur}min`;
const save = () => {
toast(`Evento atualizado ✓`);
onClose();
};
const del = () => {
if (onDelete) onDelete();
toast('Evento removido');
onClose();
};
return (
e.stopPropagation()} style={{width: 420}}>
Detalhes do evento
{evento.hora} · duração {durLabel}
{ICN.x}
setTitulo(e.target.value)} />
{evento.pessoas.slice(0,5).map((id, i) => (
{CONTATOS[id]?.nome?.split(' ')[0] || 'Participante'}
))}
);
}
function EventTimeline({ events, compact, onEventClick, nowTop, nowLabel }) {
const _nowTop = nowTop ?? (5*60+42)*PX_PER_MIN;
const _nowLabel = nowLabel ?? '13:42';
return (
{HOURS.map(h =>
{h}
)}
{_nowLabel}
{events.map((e,i) => {
const [h,m] = e.hora.split(':').map(Number);
const top = ((h-8)*60+m)*PX_PER_MIN;
const height = e.dur*PX_PER_MIN;
return (
onEventClick && onEventClick(e)}>
{e.hora} · {e.dur>=60?`${Math.floor(e.dur/60)}h${e.dur%60?' '+(e.dur%60)+'min':''}`:e.dur+'min'}
{e.pessoas.slice(0,3).map(id=>

)}
{e.pessoas.length>3&&
+{e.pessoas.length-3}}
{!compact &&
{e.titulo}
}
{compact && height > 40 &&
{e.titulo}
}
);
})}
);
}
// ── Helpers de data reais ──────────────────────────────────────────
const MONTHS_PT = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
const MONTHS_ABBR = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'];
const calHeaders = () => { const t = localStorage.getItem('ezza_token'); return { 'Content-Type':'application/json', ...(t?{'Authorization':`Bearer ${t}`}:{}) }; };
const _pad2 = n => String(n).padStart(2,'0');
const dKey = d => `${d.getFullYear()}-${_pad2(d.getMonth()+1)}-${_pad2(d.getDate())}`;
const addDaysCal = (d,n) => { const x = new Date(d); x.setDate(x.getDate()+n); return x; };
const startOfWeekSun = d => addDaysCal(d, -d.getDay());
function transformEvento(e) {
const ini = new Date(String(e.inicio).replace(' ','T'));
const fimRaw = e.fim ? new Date(String(e.fim).replace(' ','T')) : null;
const dur = fimRaw ? Math.max(15, Math.round((fimRaw - ini)/60000)) : 60;
return { id: e.id, titulo: e.titulo, hora: `${_pad2(ini.getHours())}:${_pad2(ini.getMinutes())}`, dur, color: e.cor || 'blue', pessoas: [], key: dKey(ini), inicio: ini };
}
function WeekStrip({ week, selectedKey, onSelect, byKey }) {
const todayKey = dKey(new Date());
return (
{week.map(d => {
const k = dKey(d); const evs = byKey[k] || [];
return (
onSelect(d)}>
{DOWS[d.getDay()]}
{d.getDate()}
{evs.slice(0,3).map((e,i)=>)}
);
})}
);
}
function Calendario() {
const _t0 = new Date(); _t0.setHours(0,0,0,0);
const [selectedDate, setSelectedDate] = useStateCal(_t0);
const [weekOffset, setWeekOffset] = useStateCal(0);
const [monthOffset, setMonthOffset] = useStateCal(0);
const [viewMode, setViewMode] = useStateCal('Semana');
const [showNovoEvento, setShowNovoEvento] = useStateCal(false);
const [eventos, setEventos] = useStateCal([]); // brutos do banco
const [selectedEvent, setSelectedEvent] = useStateCal(null);
const [showJumpMenu, setShowJumpMenu] = useStateCal(false);
const [now, setNow] = useStateCal(new Date());
const toast = useToast();
React.useEffect(() => {
fetch('/api/eventos', { headers: calHeaders() })
.then(r => r.json()).then(d => { if (d.ok) setEventos(d.eventos); }).catch(() => {});
const t = setInterval(() => setNow(new Date()), 30000);
return () => clearInterval(t);
}, []);
const nowH = now.getHours(), nowM = now.getMinutes();
const nowLabel = `${_pad2(nowH)}:${_pad2(nowM)}`;
const nowTop = ((nowH - 8) * 60 + nowM) * PX_PER_MIN;
// Eventos reais agrupados por dia (YYYY-MM-DD)
const byKey = {};
eventos.map(transformEvento).forEach(e => { (byKey[e.key] = byKey[e.key] || []).push(e); });
Object.values(byKey).forEach(arr => arr.sort((a,b) => a.hora.localeCompare(b.hora)));
const today0 = new Date(); today0.setHours(0,0,0,0);
const todayKey = dKey(today0);
const weekStart = addDaysCal(startOfWeekSun(today0), weekOffset*7);
const week = Array.from({length:7}, (_,i) => addDaysCal(weekStart, i));
const selKey = dKey(selectedDate);
const dayEvents = byKey[selKey] || [];
const monthLabel = `${MONTHS_PT[week[3].getMonth()]} ${week[3].getFullYear()}`;
const totalSemana = week.reduce((s,d) => s + (byKey[dKey(d)] || []).length, 0);
// Mês exibido (visão Mês)
const monthBase = new Date(today0.getFullYear(), today0.getMonth() + monthOffset, 1);
const mY = monthBase.getFullYear(), mM = monthBase.getMonth();
const monthLabelM = `${MONTHS_PT[mM]} ${mY}`;
// Próximos compromissos (de hoje em diante)
const proximos = eventos.map(transformEvento)
.filter(e => e.key >= todayKey)
.sort((a,b) => (a.key + a.hora).localeCompare(b.key + b.hora))
.slice(0, 3);
const handleCriarEvento = async (novo) => {
const inicioStr = `${selKey} ${novo.hora}:00`;
const fimD = new Date(`${selKey}T${novo.hora}:00`); fimD.setMinutes(fimD.getMinutes() + novo.dur);
const fimStr = `${dKey(fimD)} ${_pad2(fimD.getHours())}:${_pad2(fimD.getMinutes())}:00`;
try {
const r = await fetch('/api/eventos', { method:'POST', headers: calHeaders(),
body: JSON.stringify({ titulo: novo.titulo, inicio: inicioStr, fim: fimStr, tipo: 'reuniao', cor: novo.color }) });
const d = await r.json();
if (d.ok) setEventos(prev => [...prev, d.evento]);
} catch (e) {}
};
const handleDelete = async (ev) => {
if (ev && ev.id) { try { await fetch(`/api/eventos/${ev.id}`, { method:'DELETE', headers: calHeaders() }); } catch (e) {} }
setEventos(prev => prev.filter(e => e.id !== (ev && ev.id)));
setSelectedEvent(null);
};
const JUMP_OPTIONS = [
{ label: 'Hoje', icon: ICN.cal, action: () => { setWeekOffset(0); setMonthOffset(0); setSelectedDate(today0); } },
{ label: 'Semana anterior', icon: ICN.arrowLeft, action: () => setWeekOffset(w => w - 1) },
{ label: 'Próxima semana', icon: ICN.arrowRight, action: () => setWeekOffset(w => w + 1) },
{ label: 'Mês anterior', icon: ICN.arrowLeft, action: () => setMonthOffset(m => m - 1) },
{ label: 'Próximo mês', icon: ICN.arrowRight, action: () => setMonthOffset(m => m + 1) },
];
const JumpMenu = () => {
const { useEffect: useJE, useRef: useJR } = React;
const ref = useJR();
useJE(() => {
const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setShowJumpMenu(false); };
document.addEventListener('mousedown', h);
return () => document.removeEventListener('mousedown', h);
}, []);
return (
e.stopPropagation()}>
Navegar para
{JUMP_OPTIONS.map((o, i) => (
{ o.action(); setShowJumpMenu(false); toast(o.label); }}>
{o.icon}
{o.label}
))}
);
};
const selDayLabel = `${selectedDate.getDate()} ${MONTHS_ABBR[selectedDate.getMonth()]}`;
const NavBar = () => {
const isMonth = viewMode === 'Mês';
const label = isMonth ? monthLabelM : monthLabel;
const atToday = isMonth ? monthOffset === 0 : weekOffset === 0;
return (
isMonth ? setMonthOffset(m=>m-1) : setWeekOffset(w=>w-1)}/>
isMonth ? setMonthOffset(m=>m+1) : setWeekOffset(w=>w+1)}/>
{label}
{!atToday&&}
setShowNovoEvento(true)} onPeriod={() => setShowJumpMenu(j => !j)} />
{showJumpMenu && }
);
};
const ViewTabs = () => (
{['Semana','Dia','Mês'].map(v=>(
setViewMode(v)} style={{cursor:'default'}}>{v}
))}
setShowNovoEvento(true)}/>
);
const ProximosCard = () => (
{proximos.map((e)=>(
setSelectedEvent(e)} style={{cursor:'default'}}>
{e.inicio.getDate()}
{MONTHS_ABBR[e.inicio.getMonth()]}
{e.titulo}
{e.hora} · {e.dur>=60?`${Math.floor(e.dur/60)}h${e.dur%60?' '+(e.dur%60)+'min':''}`:e.dur+'min'}
{ICN.arrowRight}
))}
{proximos.length===0&&
Nenhum compromisso futuro.
}
);
const AgendaList = () => (
{dayEvents.map((e)=>(
setSelectedEvent(e)} style={{cursor:'default'}}>
{e.titulo}
{e.hora} · {e.dur>=60?`${Math.floor(e.dur/60)}h${e.dur%60?' '+(e.dur%60)+'min':''}`:e.dur+'min'}
))}
{dayEvents.length===0&&
Nenhum evento neste dia.
}
);
// ── Visão MÊS ──────────────────────────────────────────────────
if (viewMode === 'Mês') {
const first = new Date(mY, mM, 1);
const startPad = first.getDay();
const dim = new Date(mY, mM+1, 0).getDate();
const cells = [];
for (let i=0;i
Calendário mensal>} subtitle={`${monthLabelM} · visão geral`} right={} />
{monthWeeks.map((wk,wi)=>
wk.map((day,di)=>{
const k = day ? dKey(day) : null;
const evs = k ? (byKey[k]||[]) : [];
return (
day&&setSelectedDate(day)}>
{day&&<>
{day.getDate()}
{evs.length>0&&(
{evs.slice(0,3).map((e,i)=>)}
)}
>}
);
})
)}
{showNovoEvento && setShowNovoEvento(false)} onCreate={handleCriarEvento} />}
{selectedEvent && setSelectedEvent(null)} onDelete={() => handleDelete(selectedEvent)} />}
>
);
}
// ── Visão DIA ───────────────────────────────────────────────────
if (viewMode === 'Dia') {
return (
<>
Calendário do dia>} subtitle={`${selDayLabel} · ${dayEvents.length} evento${dayEvents.length!==1?'s':''}`} right={} />
{selDayLabel} — agenda
{dayEvents.length} eventos
{showNovoEvento && setShowNovoEvento(false)} onCreate={handleCriarEvento} />}
{selectedEvent && setSelectedEvent(null)} onDelete={() => handleDelete(selectedEvent)} />}
>
);
}
// ── Visão SEMANA (padrão) ────────────────────────────────────────
return (
<>
Calendário da semana>} subtitle={`${monthLabel} · ${totalSemana} compromisso${totalSemana!==1?'s':''} esta semana`} right={} />
{week.map(d=>{
const k = dKey(d); const evs = byKey[k] || [];
return (
{evs.slice(0,3).map((e,i)=>(
{e.titulo}
))}
{evs.length===0&&
}
);
})}
{selDayLabel}
{dayEvents.length} eventos
{showNovoEvento && setShowNovoEvento(false)} onCreate={handleCriarEvento} />}
{selectedEvent && setSelectedEvent(null)} onDelete={() => handleDelete(selectedEvent)} />}
>
);
}
const CAL_STYLES = `
/* ── Jump menu (navegar para) ── */
.jump-menu {
position: absolute; right: 0; top: calc(100% + 6px);
background: white; border-radius: 16px; padding: 8px;
box-shadow: 0 8px 32px -8px rgba(20,30,60,.22), 0 0 0 0.5px rgba(20,30,60,.08);
z-index: 50; min-width: 190px; animation: fadein .15s ease;
}
.jump-menu-title {
font-size: 10px; font-weight: 700; text-transform: uppercase;
letter-spacing: 0.1em; color: var(--ink-muted); padding: 4px 10px 8px;
}
.jump-menu-item {
display: flex; align-items: center; gap: 10px;
padding: 9px 12px; border-radius: 10px; cursor: default;
font-size: 13px; font-weight: 500; transition: background .1s;
}
.jump-menu-item:hover { background: oklch(0.96 0.01 250); }
html.theme-dark .jump-menu { background: oklch(0.24 0.012 255); }
html.theme-dark .jump-menu-item:hover { background: oklch(0.30 0.012 255); }
/* ── Lista de eventos no sidebar ── */
.ev-list { display: flex; flex-direction: column; gap: 6px; }
.ev-list-item {
display: flex; align-items: stretch; gap: 10px;
background: oklch(0.975 0.008 250); border-radius: 12px;
padding: 10px 12px; transition: background .12s, transform .1s;
}
.ev-list-item:hover { background: oklch(0.96 0.015 250); transform: translateX(2px); }
.ev-list-stripe {
width: 3px; border-radius: 99px; flex-shrink: 0; align-self: stretch; min-height: 36px;
}
.ev-stripe-blue { background: var(--acc-blue); }
.ev-stripe-pink { background: var(--acc-pink); }
.ev-stripe-yellow { background: var(--acc-yellow); }
.ev-stripe-mint { background: var(--acc-mint); }
.ev-stripe-lav { background: var(--acc-lav); }
html.theme-dark .ev-list-item { background: oklch(0.28 0.012 255); }
html.theme-dark .ev-list-item:hover { background: oklch(0.31 0.012 255); }
.cal-grid { display: grid; grid-template-columns: 1.7fr 1fr; gap: 14px; align-items: start; }
.week-strip { display: grid; grid-template-columns: repeat(7,1fr); gap: 8px; margin-bottom: 18px; }
.wday { background: oklch(0.97 0.01 250/0.7); border-radius: 16px; padding: 10px 6px; text-align: center; cursor: default; transition: background .15s; }
.wday:hover { background: oklch(0.94 0.02 250); }
.wday-dow { font-size: 10px; font-weight: 600; color: var(--ink-muted); letter-spacing: 0.08em; }
.wday-num { font-size: 22px; font-weight: 700; margin-top: 4px; letter-spacing: -0.02em; }
.wday-dots { display: flex; gap: 3px; justify-content: center; margin-top: 4px; min-height: 6px; }
.wdot { width: 5px; height: 5px; border-radius: 50%; }
.wdot-blue { background: var(--acc-blue); }
.wdot-pink { background: var(--acc-pink); }
.wdot-yellow { background: var(--acc-yellow); }
.wdot-mint { background: var(--acc-mint); }
.wdot-lav { background: var(--acc-lav); }
.wday-sel { background: var(--black) !important; color: white; }
.wday-sel .wday-dow { color: rgba(255,255,255,.65); }
.day-grid { display: grid; grid-template-columns: 64px 1fr; gap: 8px; position: relative; }
.hours { display: flex; flex-direction: column; position: relative; }
.hour-row { height: 60px; border-top: 1px dashed oklch(0.90 0.01 255); color: var(--ink-faint); font-size: 10px; padding-top: 2px; }
.hour-row:last-child { border-bottom: 1px dashed oklch(0.90 0.01 255); }
.events-col { position: relative; border-top: 1px dashed oklch(0.90 0.01 255); border-bottom: 1px dashed oklch(0.90 0.01 255); }
.events-col::before { content:''; position:absolute; inset:0; background-image: repeating-linear-gradient(to bottom, transparent 0, transparent 59px, oklch(0.92 0.01 255) 59px, oklch(0.92 0.01 255) 60px); pointer-events:none; }
.event { position:absolute; left:4px; right:4px; border-radius:12px; padding:8px 12px; box-shadow:0 6px 14px -10px rgba(20,30,60,.18); display:flex; flex-direction:column; gap:6px; overflow:hidden; z-index:1; cursor:default; transition: transform .15s; }
.event:hover { transform: scale(1.01); }
.event-blue { background:oklch(0.92 0.07 250); color:oklch(0.30 0.16 250); border-left:3px solid oklch(0.55 0.16 250); }
.event-pink { background:oklch(0.92 0.07 18); color:oklch(0.36 0.15 18); border-left:3px solid oklch(0.62 0.18 25); }
.event-yellow { background:oklch(0.95 0.10 95); color:oklch(0.34 0.13 80); border-left:3px solid oklch(0.74 0.15 75); }
.event-mint { background:oklch(0.93 0.07 160); color:oklch(0.32 0.12 160); border-left:3px solid oklch(0.55 0.13 160); }
.event-lav { background:oklch(0.93 0.06 295); color:oklch(0.34 0.13 295); border-left:3px solid oklch(0.58 0.15 295); }
.event-meta { display:flex; justify-content:space-between; align-items:center; }
.event-time { font-size:10.5px; font-weight:600; opacity:0.8; }
.event-title { font-size:12.5px; font-weight:600; line-height:1.25; }
.now-line { position:absolute; left:0; right:-8px; height:0; z-index:2; pointer-events:none; }
.now-line .now-dot { position:absolute; right:-6px; top:-5px; width:10px; height:10px; border-radius:50%; background:var(--danger); border:2px solid white; }
.now-line .now-time { position:absolute; left:0; top:-7px; font-size:9.5px; font-weight:700; color:var(--danger); background:white; padding:1px 4px; border-radius:4px; }
.now-line-ev { position:absolute; left:0; right:0; height:2px; background:var(--danger); z-index:3; pointer-events:none; border-radius:2px; }
.upc { display:flex; align-items:center; gap:12px; padding:6px 0; cursor:default; }
.upc-date { width:48px; height:48px; border-radius:14px; text-align:center; padding:6px 0; flex-shrink:0; }
.upc-blue { background:oklch(0.92 0.07 250); color:oklch(0.30 0.16 250); }
.upc-pink { background:oklch(0.92 0.07 18); color:oklch(0.36 0.15 18); }
.upc-yellow { background:oklch(0.95 0.10 95); color:oklch(0.34 0.13 80); }
.upc-d { font-size:16px; font-weight:700; line-height:1; }
.upc-m { font-size:10px; font-weight:600; margin-top:2px; opacity:0.75; text-transform:uppercase; }
/* visão semana multi-col */
.week-multi-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:6px; margin-bottom:4px; }
.week-col-ev { min-height:48px; border-radius:10px; padding:4px; display:flex; flex-direction:column; gap:3px; background:oklch(0.98 0.005 250); }
.week-col-sel { background:oklch(0.96 0.015 250); }
.week-ev-pill { font-size:9.5px; font-weight:600; border-radius:6px; padding:3px 6px; line-height:1.2; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.week-ev-blue { background:oklch(0.92 0.07 250); color:oklch(0.30 0.16 250); }
.week-ev-pink { background:oklch(0.92 0.07 18); color:oklch(0.36 0.15 18); }
.week-ev-yellow { background:oklch(0.95 0.10 95); color:oklch(0.34 0.13 80); }
.week-ev-mint { background:oklch(0.93 0.07 160); color:oklch(0.32 0.12 160); }
.week-ev-lav { background:oklch(0.93 0.06 295); color:oklch(0.34 0.13 295); }
.week-ev-empty { height:20px; }
/* visão mês */
.month-dow-header { display:grid; grid-template-columns:repeat(7,1fr); gap:4px; margin-bottom:6px; }
.month-dow { text-align:center; font-size:10px; font-weight:700; color:var(--ink-muted); letter-spacing:0.08em; padding:4px 0; }
.month-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:4px; }
.month-cell { min-height:72px; border-radius:12px; padding:8px; background:oklch(0.98 0.005 250); cursor:default; transition:background .15s; display:flex; flex-direction:column; gap:4px; }
.month-cell:hover:not(.month-cell-empty) { background:oklch(0.94 0.02 250); }
.month-cell-empty { background:transparent; }
.month-cell-today { background:oklch(0.95 0.02 250); border:1.5px solid var(--acc-blue); }
.month-cell-sel { background:var(--black) !important; color:white; }
.month-day-num { font-size:13px; font-weight:700; letter-spacing:-0.01em; }
.month-cell-sel .month-day-num { color:white; }
.month-dots { display:flex; gap:3px; flex-wrap:wrap; }
`;
window.Calendario = Calendario;