← 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>