← back to Butlr
views/public/calendar.ejs
152 lines
<%- include('partials/head') %>
<body>
<%- include('partials/header') %>
<main class="wrap" style="max-width:1100px;margin:24px auto;padding:0 18px;">
<style>
.cal-head { display:flex; align-items:center; gap:16px; margin:18px 0 12px; flex-wrap:wrap; }
.cal-head h1 { font:500 22px/1 var(--serif,'Cormorant Garamond',Georgia,serif); margin:0; flex:1; }
.cal-head .nav { display:flex; gap:6px; align-items:center; }
.cal-head .nav a { padding:5px 11px; border:1px solid var(--line,#d2cdc3); border-radius:5px; text-decoration:none; color:var(--ink,#1a1816); background:transparent; font:600 12px var(--sans,system-ui); }
.cal-head .nav a.today { background:var(--ink,#1a1816); color:var(--bg,#f7f3eb); }
.cal-head .schedule-btn { padding:8px 14px; background:#3a8a5a; color:#fff; border-radius:5px; text-decoration:none; font:600 12px var(--sans,system-ui); letter-spacing:.04em; }
.cal-grid { display:grid; grid-template-columns:repeat(7,1fr); gap:1px; background:var(--line,#d2cdc3); border:1px solid var(--line,#d2cdc3); border-radius:6px; overflow:hidden; }
.cal-dow { background:var(--card-bg,#f4f0e8); color:var(--ink-faint,#7a6e5a); font:600 10px var(--sans,system-ui); letter-spacing:.18em; text-transform:uppercase; padding:8px; text-align:center; }
.cal-cell { background:var(--bg,#fff); min-height:106px; padding:6px 8px; position:relative; }
.cal-cell.out { background:var(--card-bg,#f8f5ee); opacity:.4; }
.cal-cell.today { background:#fffbe8; }
.cal-cell .date { font:600 11px var(--sans,system-ui); color:var(--ink-faint,#7a6e5a); }
.cal-cell.today .date { color:#a8821b; }
.pill { display:flex; align-items:center; gap:4px; font:11px/1.2 var(--sans,system-ui); padding:3px 6px; margin-top:4px; border-radius:3px; overflow:hidden; cursor:pointer; text-decoration:none; }
.pill .time { font-weight:600; opacity:.7; min-width:36px; }
.pill .name { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.pill.sched { background:#dcfce7; color:#14532d; border-left:3px solid #16a34a; }
.pill.recur { background:#dbeafe; color:#1e3a8a; border-left:3px solid #2563eb; }
.pill.done { background:#f0eee5; color:#3a2f1c; border-left:3px solid #7a6e5a; }
.pill.fail { background:#fee2e2; color:#900; border-left:3px solid #d33; }
.legend { display:flex; gap:14px; margin:12px 0 0; font:11px var(--sans,system-ui); color:var(--ink-faint,#7a6e5a); flex-wrap:wrap; }
.legend .dot { display:inline-block; width:10px; height:10px; border-radius:50%; margin-right:5px; vertical-align:middle; }
</style>
<div class="cal-head">
<h1><%= ['','Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][month] %> <%= year %></h1>
<div class="nav">
<%
const pY = month === 1 ? year-1 : year;
const pM = month === 1 ? 12 : month-1;
const nY = month === 12 ? year+1 : year;
const nM = month === 12 ? 1 : month+1;
const today = new Date();
%>
<a href="/calendar?year=<%= pY %>&month=<%= pM %>">← prev</a>
<a class="today" href="/calendar">today</a>
<a href="/calendar?year=<%= nY %>&month=<%= nM %>">next →</a>
</div>
<a href="/new" class="schedule-btn">+ Schedule a call</a>
</div>
<div class="cal-grid">
<% ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].forEach(d => { %><div class="cal-dow"><%= d %></div><% }); %>
<%
const monthStart = new Date(year, month-1, 1);
const startDow = monthStart.getDay();
const daysInMonth = new Date(year, month, 0).getDate();
const prevDays = new Date(year, month-1, 0).getDate();
// Bucket events by yyyy-mm-dd
const byDay = {};
(scheduled || []).forEach(r => {
const t = new Date(r.next_fire_at || r.last_fire_at || 0);
const key = `${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,'0')}-${String(t.getDate()).padStart(2,'0')}`;
(byDay[key] = byDay[key] || []).push({ kind: r.cron_expr ? 'recur' : 'sched', t, ref: r });
});
(pastCalls || []).forEach(c => {
const t = new Date(c.created_at);
const key = `${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,'0')}-${String(t.getDate()).padStart(2,'0')}`;
(byDay[key] = byDay[key] || []).push({ kind: c.status === 'failed' ? 'fail' : 'done', t, call: c });
});
// Render 6 weeks × 7 days = 42 cells
for (let i = 0; i < 42; i++) {
const dayNum = i - startDow + 1;
let cellYear, cellMonth, cellDay, outOfMonth;
if (dayNum < 1) { outOfMonth = true; cellDay = prevDays + dayNum; cellYear = month === 1 ? year-1 : year; cellMonth = month === 1 ? 12 : month-1; }
else if (dayNum > daysInMonth) { outOfMonth = true; cellDay = dayNum - daysInMonth; cellYear = month === 12 ? year+1 : year; cellMonth = month === 12 ? 1 : month+1; }
else { outOfMonth = false; cellDay = dayNum; cellYear = year; cellMonth = month; }
const key = `${cellYear}-${String(cellMonth).padStart(2,'0')}-${String(cellDay).padStart(2,'0')}`;
const isToday = !outOfMonth && cellYear === today.getFullYear() && cellMonth === today.getMonth()+1 && cellDay === today.getDate();
const items = byDay[key] || [];
items.sort((a,b) => a.t - b.t);
%>
<div class="cal-cell <%= outOfMonth ? 'out' : '' %> <%= isToday ? 'today' : '' %>">
<div class="date"><%= cellDay %></div>
<% items.slice(0, 4).forEach(it => {
const time = it.t.toTimeString().slice(0,5);
const name = it.kind === 'done' || it.kind === 'fail'
? (it.call.business_name || it.call.business_phone || '?')
: (it.ref.business_name || it.ref.business_phone || '?');
const href = it.kind === 'done' || it.kind === 'fail'
? `/listen/${it.call.id}`
: `#sched-${it.ref.id}`;
%>
<a class="pill <%= it.kind %>" href="<%= href %>" title="<%= name %> · <%= time %>">
<span class="time"><%= time %></span><span class="name"><%= name %></span>
</a>
<% }); %>
<% if (items.length > 4) { %>
<div style="font:10px var(--sans,system-ui); color:var(--ink-faint,#7a6e5a); margin-top:2px;">+<%= items.length - 4 %> more</div>
<% } %>
</div>
<% } %>
</div>
<div class="legend">
<span><span class="dot" style="background:#16a34a"></span>One-shot scheduled</span>
<span><span class="dot" style="background:#2563eb"></span>Recurring (cron)</span>
<span><span class="dot" style="background:#7a6e5a"></span>Past · done</span>
<span><span class="dot" style="background:#d33"></span>Past · failed</span>
</div>
<% if ((scheduled || []).length > 0) { %>
<h2 style="font:500 18px/1.2 var(--serif,'Cormorant Garamond',Georgia,serif); margin:28px 0 10px;">Scheduled this month</h2>
<table style="width:100%; border-collapse:collapse; font:13px var(--sans,system-ui);">
<thead>
<tr style="text-align:left; color:var(--ink-faint,#7a6e5a); font:600 10px var(--sans,system-ui); letter-spacing:.18em; text-transform:uppercase;">
<th style="padding:8px 6px; border-bottom:1px solid var(--line,#d2cdc3);">When</th>
<th style="padding:8px 6px; border-bottom:1px solid var(--line,#d2cdc3);">Who</th>
<th style="padding:8px 6px; border-bottom:1px solid var(--line,#d2cdc3);">Goal</th>
<th style="padding:8px 6px; border-bottom:1px solid var(--line,#d2cdc3);">Kind</th>
<th style="padding:8px 6px; border-bottom:1px solid var(--line,#d2cdc3);"></th>
</tr>
</thead>
<tbody>
<% scheduled.forEach(r => { %>
<tr id="sched-<%= r.id %>" style="border-bottom:1px solid var(--line,#d2cdc3);">
<td style="padding:8px 6px;"><%= new Date(r.next_fire_at).toLocaleString() %></td>
<td style="padding:8px 6px;"><strong><%= r.business_name || '?' %></strong><br><span style="color:var(--ink-faint,#7a6e5a); font-size:11px;"><%= r.business_phone %></span></td>
<td style="padding:8px 6px; max-width:280px; font-size:12px; color:var(--ink-faint,#7a6e5a);"><%= (r.goal||'').slice(0,120) %><%= (r.goal||'').length>120?'…':'' %></td>
<td style="padding:8px 6px;"><%= r.cron_expr ? 'every: ' + r.cron_expr : 'one-shot' %></td>
<td style="padding:8px 6px; text-align:right;">
<button data-cancel="<%= r.id %>" style="padding:5px 11px; background:transparent; border:1px solid #d33; color:#d33; border-radius:4px; font:600 11px var(--sans,system-ui); cursor:pointer;">Cancel</button>
</td>
</tr>
<% }); %>
</tbody>
</table>
<script>
document.querySelectorAll('[data-cancel]').forEach(b => b.addEventListener('click', async () => {
if (!confirm('Cancel this scheduled call?')) return;
const r = await fetch('/api/scheduled/' + b.dataset.cancel, { method: 'DELETE' });
if (r.ok) location.reload();
else alert('Cancel failed: ' + (await r.text()));
}));
</script>
<% } %>
</main>
<%- include('partials/footer') %>
</body></html>