/* global React, Icon, Avatar */
const { useState } = React;
function Calendar({ goto }) {
const [view, setView] = useState('week');
const [selectedEvent, setSelectedEvent] = useState(null);
const events = window.CALENDAR_EVENTS;
const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
const dates = ['27', '28', '29', '30', '01', '02', '03'];
const hours = Array.from({ length: 11 }, (_, i) => i + 8); // 8am-6pm
const eventColor = {
discovery: '#f97316',
strategy: '#eab308',
internal: '#3b82f6',
other: '#6b7280',
};
return (
{view === 'week' ? (
{days.map((d, i) => (
))}
{hours.map(h => (
{String(h).padStart(2,'0')}:00
{days.map((d, dayIdx) => {
const dayEvents = events.filter(e => e.day === dayIdx && Math.floor(e.start) === h);
return (
{dayEvents.map(e => (
setSelectedEvent(e)} style={{
background: `${eventColor[e.type]}22`,
border: '1px solid ' + eventColor[e.type],
borderLeft: '2px solid ' + eventColor[e.type],
padding: 6, fontSize: 10, cursor: 'pointer',
height: `${(e.end - e.start) * 56 - 8}px`,
minHeight: 28,
}}>
{e.title}
{String(Math.floor(e.start)).padStart(2,'0')}:{String(Math.round((e.start%1)*60)).padStart(2,'0')}
))}
);
})}
))}
) : (
)}
{selectedEvent && (
<>
setSelectedEvent(null)}>
Event
{selectedEvent.title}
{days[selectedEvent.day]} · {String(Math.floor(selectedEvent.start)).padStart(2,'0')}:{String(Math.round((selectedEvent.start%1)*60)).padStart(2,'0')} – {String(Math.floor(selectedEvent.end)).padStart(2,'0')}:{String(Math.round((selectedEvent.end%1)*60)).padStart(2,'0')}
{selectedEvent.contactId && (() => {
const c = window.CONTACTS.find(x => x.id === selectedEvent.contactId);
return c && (
Contact
goto('contact', c.id)} style={{ padding: 12, background: 'var(--raised)', border: '1px solid var(--divider)', display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer' }}>
{c.name}
{c.title} · {c.company}
);
})()}
{selectedEvent.invite &&
}
{selectedEvent.reminders &&
}
Prep notes
{selectedEvent.notes}
>
)}
);
}
function MonthView({ events, eventColor, onSelect }) {
const days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
// Build a 5x7 grid for April. Today is Apr 28 (col idx 1, row idx 4 if Mon=0)
const cells = [];
for (let i = 0; i < 35; i++) {
const dayNum = i - 1; // April starts on Wed = idx 2
let label = '';
if (i >= 2 && i < 32) label = String(i - 1);
cells.push({ idx: i, label, isToday: label === '28' });
}
return (
{days.map((d, i) => (
))}
{cells.map((c, i) => {
const colIdx = i % 7;
const rowIdx = Math.floor(i / 7);
// place this week's events on row 4 (week of Apr 27)
const eventsHere = (rowIdx === 4 && c.label) ? events.filter(e => e.day === colIdx) : [];
return (
{c.label || ''}
{eventsHere.map(e => (
onSelect(e)} style={{ padding: '3px 6px', marginBottom: 3, background: `${eventColor[e.type]}22`, borderLeft: '2px solid ' + eventColor[e.type], fontSize: 10, cursor: 'pointer', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
{String(Math.floor(e.start)).padStart(2,'0')}:{String(Math.round((e.start%1)*60)).padStart(2,'0')}{' '}
{e.title.split(' · ')[0]}
))}
);
})}
);
}
Object.assign(window, { Calendar });
/* ============================================================
INVITE STATUS — did the contact accept the Google Calendar invite?
============================================================ */
function InviteStatus({ invite }) {
const cfg = {
accepted: { label: 'Accepted', color: '#22c55e', icon: 'check', bg: 'rgba(34,197,94,0.10)' },
tentative: { label: 'Maybe', color: '#eab308', icon: 'clock', bg: 'rgba(234,179,8,0.10)' },
declined: { label: 'Declined', color: '#ef4444', icon: 'x', bg: 'rgba(239,68,68,0.10)' },
pending: { label: 'No response', color: 'var(--text-dim)', icon: 'mail', bg: 'var(--raised)' },
}[invite.status] || { label: invite.status, color: 'var(--text-secondary)', icon: 'mail', bg: 'var(--raised)' };
return (
Google Calendar Invite
{cfg.label}
{invite.status === 'pending'
? 'Awaiting response from contact'
: `Responded ${invite.respondedAt}${invite.note ? ' · ' + invite.note : ''}`}
);
}
/* ============================================================
REMINDER SEQUENCE — 24h, 8h, 1h, 10m before
============================================================ */
const REM_OFFSET_LABEL = {
'24h': '24 hours before',
'8h': '8 hours before',
'1h': '1 hour before',
'10m': '10 minutes before',
};
const REM_STATUS = {
scheduled: { label: 'Scheduled', color: 'var(--text-dim)', icon: 'clock' },
sent: { label: 'Sent', color: 'var(--text-secondary)', icon: 'check' },
delivered: { label: 'Delivered', color: 'var(--text-secondary)', icon: 'check' },
opened: { label: 'Opened', color: '#22c55e', icon: 'eye' },
read: { label: 'Read', color: '#22c55e', icon: 'eye' },
cancelled: { label: 'Cancelled', color: '#ef4444', icon: 'x' },
failed: { label: 'Failed', color: '#ef4444', icon: 'x' },
};
function ReminderSequence({ event }) {
const reminders = event.reminders || [];
const sentCount = reminders.filter(r => ['sent','delivered','opened','read'].includes(r.status)).length;
const openedCount = reminders.filter(r => ['opened','read'].includes(r.status)).length;
return (
Reminder Sequence
{sentCount}/{reminders.length} sent · {openedCount} opened
{reminders.map((r, i) => {
const s = REM_STATUS[r.status] || REM_STATUS.scheduled;
const channelColor = r.channel === 'whatsapp' ? '#22c55e' : '#3b82f6';
return (
{/* Channel badge */}
{/* Offset + detail */}
{REM_OFFSET_LABEL[r.offset] || r.offset}
· {r.channel}
{r.detail || (r.sentAt ? r.sentAt : '—')}
{/* Status pill */}
{s.label}
);
})}
);
}
Object.assign(window, { InviteStatus, ReminderSequence });