/* global React */ /* ============================================================================= ACTIONS DASHBOARD v2 — backed by the `actions` table 3-column board + DetailDrawer + TriageDrawer + Archive Done ========================================================================== */ const { useState, useRef, useEffect, useCallback } = React; const TASK_DOMAINS = { creative: { label: "Creative", ico: "fire-1", color: "var(--axis-creative)" }, physical: { label: "Physical", ico: "cross", color: "var(--axis-physical)" }, inner: { label: "Inner World", ico: "hold-eye", color: "var(--axis-inner)" }, interpersonal: { label: "Interpersonal", ico: "book", color: "var(--axis-interpersonal)" }, adventure: { label: "Adventure", ico: "hold-globe", color: "var(--axis-adventure)" }, admin: { label: "Life Admin", ico: "settings", color: "var(--axis-life-admin)" }, aligned: { label: "True North", ico: "star-maps", color: "var(--gold-1)" }, }; /* ── Anchored Ledger — Value Velocity panel with nested practices ───────────── Frozen creation order (never sorted by velocity). Caret expands practices in place (multi-open). Row/bar click opens the ValueDrawer (8-week trend + evidence). Uncharted (no data yet) renders distinct from quiet (measured 0). */ const LEDGER_WD = ["S", "M", "T", "W", "T", "F", "S"]; const ledgerWd = (iso) => LEDGER_WD[new Date(iso + "T00:00:00").getDay()]; const LEDGER_DAY_FULL = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; const LEDGER_MONTH_SHT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; const ledgerDayLabel = (iso, isToday) => { if (isToday) return 'Today'; const d = new Date(iso + 'T00:00:00'); return LEDGER_DAY_FULL[d.getDay()] + ', ' + LEDGER_MONTH_SHT[d.getMonth()] + ' ' + d.getDate(); }; function LedgerFlat({ rows, unanchored, win, onLogDay }) { const rowsRef = useRef(null); const [colPos, setColPos] = useState(null); useEffect(() => { if (!rowsRef.current) return; const cell = rowsRef.current.querySelector('.ledger-cell.is-today'); if (!cell) return; const rr = rowsRef.current.getBoundingClientRect(); const cr = cell.getBoundingClientRect(); setColPos({ left: cr.left - rr.left, width: cr.width }); }, [win.today_index, win.days.length]); const flat = []; rows.forEach((r) => r.practices.forEach((p) => flat.push({ ...p, value: r.name, domain: p.domain || r.domain }))); (unanchored || []).forEach((p) => flat.push({ ...p, value: '— unanchored' })); return (
Practice · Value {win.days.map((d, di) => ( {ledgerWd(d)} {new Date(d + 'T00:00:00').getDate()} ))}
{colPos &&
); } function PracticeGrid({ r, win, meta, color }) { const gridRef = useRef(null); const [colPos, setColPos] = useState(null); useEffect(() => { if (!gridRef.current) return; const cell = gridRef.current.querySelector('.ledger-cell.is-today'); if (!cell) return; const rr = gridRef.current.getBoundingClientRect(); const cr = cell.getBoundingClientRect(); setColPos({ left: cr.left - rr.left, width: cr.width }); }, [win.today_index, win.days.length]); return (
{colPos && ); } function ValueLedgerPanel({ ledger, expanded, onToggle, onDetail, velocityWindow, totalEvents, domainFilter, onLogDay }) { const allRows = ledger.rows || []; const rows = (domainFilter && domainFilter.length) ? allRows.filter((r) => domainFilter.includes(r.domain)) : allRows; const win = ledger.window || { days: [], today_index: 6 }; const maxCount = Math.max(1, ...rows.map((r) => r.count || 0)); const windowLabel = velocityWindow === '30d' ? '30 days' : '7 days'; const [mode, setMode] = React.useState(() => (typeof location !== 'undefined' && location.hash.includes('flat')) ? 'allPractices' : 'byValue'); return (
Momentum Log {mode === 'byValue' && (
{mode === 'byValue' ? (
{rows.map((r) => { const meta = TASK_DOMAINS[r.domain] || {}; const color = meta.color || 'var(--gold-1)'; const open = expanded.has(r.value_id); const item = { valueGoalId: r.value_id, label: r.name, domain: r.domain, count: r.count, todayCount: r.today_count, trend: r.trend, percent: r.percent, primaryDomain: r.domain }; return (
{r.state === 'uncharted' ? ( — uncharted ) : ( )}
{open && ( )}
); })}
) : ( domainFilter.includes(p.domain)) : ledger.unanchored} /> )}
); } const COLUMN_DEFS = [ { id: "acceptance", title: "Inbox", sub: "New · drag or click to sort", eye: "INBOX", empty: "All clear" }, { id: "focus_zone", title: "Today", sub: "Execute now", eye: "TODAY", empty: "Nothing scheduled — pull from Soon or capture" }, { id: "deadline_bound", title: "Soon", sub: "Time-sensitive", eye: "SOON", empty: "Time-sensitive actions land here" }, { id: "value_aligned", title: "Vision Board", sub: "Future intent", eye: "VISION", empty: "Future intent lives here" }, ]; const HORIZON_TO_COL = { focus_zone: "focus_zone", deadline_bound: "deadline_bound", value_aligned: "value_aligned" }; const COL_TO_HORIZON = { focus_zone: "focus_zone", deadline_bound: "deadline_bound", value_aligned: "value_aligned" }; const ACTION_COLS = ["focus_zone", "deadline_bound", "value_aligned"]; const SORT_OPTIONS = [ { key: "manual", label: "Manual" }, { key: "alignment", label: "Alignment ↓" }, { key: "oldest", label: "Oldest first" }, ]; const CAPTURE_DOMAINS = ["physical", "inner", "creative", "interpersonal", "adventure", "admin"]; const DASH2_DOMAIN_ORDER = ['inner', 'physical', 'creative', 'interpersonal', 'adventure', 'admin']; const DASH2_VALUE_VELOCITY_FALLBACK = [ { label: 'HARM', percent: 42, domain: 'inner' }, { label: 'BODY', percent: 15, domain: 'physical' }, ]; function clampPct(n) { const v = Number.isFinite(n) ? n : 0; return Math.max(0, Math.min(100, Math.round(v))); } function renderValueVelocityRows(values) { if (!Array.isArray(values) || !values.length) return DASH2_VALUE_VELOCITY_FALLBACK; return values.slice(0, 6).map((v, idx) => { const pct = clampPct((v.velocity || v.aura_level || v.percent || 0) * 100); const domain = v.primary_domain || v.domain || DASH2_DOMAIN_ORDER[idx % DASH2_DOMAIN_ORDER.length]; return { label: (v.name || v.label || 'Value').toUpperCase(), percent: pct, domain, }; }); } /* ── Date utilities (shared by CaptureModal + DueDatePrompt) ─────────────── */ const _pad = n => String(n).padStart(2, '0'); const _fmt = d => `${d.getFullYear()}-${_pad(d.getMonth()+1)}-${_pad(d.getDate())}`; const _today = () => { const d = new Date(); d.setHours(0,0,0,0); return d; }; const _inSevenDays = () => { const d = _today(); d.setDate(d.getDate() + 7); return d; }; const _inThirtyDays = () => { const d = _today(); d.setDate(d.getDate() + 30); return d; }; const _relativeLabel = (dateStr) => { const d = new Date(dateStr + 'T00:00:00'); const today = _today(); const diff = Math.round((d - today) / 86400000); const abs = d.toLocaleDateString('en-US', {month: 'short', day: 'numeric'}); if (diff < 0) return `Overdue (${abs})`; if (diff === 0) return `Due today (${abs})`; if (diff === 1) return `Due tomorrow (${abs})`; return `Due in ${diff} days (${abs})`; }; const parseMonthDay = (raw) => { const MONTHS = {jan:1,feb:2,mar:3,apr:4,may:5,jun:6,jul:7,aug:8,sep:9,oct:10,nov:11,dec:12}; const s = (raw || '').trim().toLowerCase(); let month, day; const slashMatch = s.match(/^(\d{1,2})[/-](\d{1,2})$/); if (slashMatch) { month = +slashMatch[1]; day = +slashMatch[2]; } const alphaFirst = s.match(/^([a-z]{3,9})\s+(\d{1,2})$/); if (alphaFirst) { month = MONTHS[alphaFirst[1].slice(0,3)]; day = +alphaFirst[2]; } const alphaLast = s.match(/^(\d{1,2})\s+([a-z]{3,9})$/); if (alphaLast) { month = MONTHS[alphaLast[2].slice(0,3)]; day = +alphaLast[1]; } if (!month || !day || month < 1 || month > 12 || day < 1 || day > 31) return null; const today = _today(); const candidate = new Date(today.getFullYear(), month - 1, day); if (candidate < today) candidate.setFullYear(today.getFullYear() + 1); return _fmt(candidate); }; /* ── helpers ──────────────────────────────────────────────────────────────── */ function getDueDateState(due) { if (!due) return null; const today = new Date(); today.setHours(0,0,0,0); const d = new Date(due + 'T00:00:00'); const delta = Math.round((d - today) / 86400000); if (delta < 0) return 'overdue'; if (delta === 0) return 'today'; if (delta <= 2) return 'impending'; return 'healthy'; } function formatDue(due, state) { if (!due) return ''; if (state === 'today') return 'Due today'; const d = new Date(due + 'T00:00:00'); const now = new Date(); now.setHours(0,0,0,0); const delta = Math.round((d - now) / 86400000); if (state === 'overdue') { const days = Math.abs(delta); if (days >= 14) return `Overdue by ${Math.round(days / 7)} weeks`; if (days >= 7) return 'Overdue by 1 week'; return days === 1 ? 'Overdue by 1 day' : `Overdue by ${days} days`; } if (state === 'impending') { return delta === 1 ? 'Due tomorrow' : `Due in ${delta} days`; } // healthy return delta === 1 ? 'Due tomorrow' : `Due in ${delta} days`; } /* ── DueDateChip ─────────────────────────────────────────────────────────── */ const DueDateChip = ({ due }) => { const state = getDueDateState(due); if (!state) return null; return ( {formatDue(due, state)} ); }; /* ── TaskCard ────────────────────────────────────────────────────────────── */ const BODY_TRUNCATE = 140; const TaskCard = ({ task, dragging, realizing, dismissing, onDragStart, onDragEnd, onRealize, onDismiss, compact, onTitleClick, needsDate, showPrompt, onDateResolved, onMove }) => { const dom = TASK_DOMAINS[task.domain] || TASK_DOMAINS.admin; const inDrawer = task.column === "drawer"; const rawBody = task.body || ''; const truncatedBody = !compact && rawBody.length > BODY_TRUNCATE ? rawBody.slice(0, BODY_TRUNCATE) + '…' : rawBody; const [editingDate, setEditingDate] = useState(false); const trayOpen = !inDrawer && (showPrompt || editingDate); const openDrawer = (e) => { e.stopPropagation(); if (!inDrawer) onTitleClick && onTitleClick(task); }; return (
= 0.85 ? " is-high-resonance" : "") + (needsDate ? " needs-date" : "") } draggable={!realizing && !dismissing && !inDrawer} tabIndex={0} role="listitem" aria-label={`${dom.label} action: ${task.title}`} data-task-id={task.id} data-due-state={getDueDateState(task.due) || ''} onDragStart={(e) => onDragStart && onDragStart(e, task)} onDragEnd={onDragEnd} onKeyDown={(e) => { if (inDrawer) return; if (e.key === " " || e.key === "Enter") { e.preventDefault(); onRealize && onRealize(task.id); } else if (e.key === "ArrowRight") { e.preventDefault(); onMove && onMove(task.id, 1); } else if (e.key === "ArrowLeft") { e.preventDefault(); onMove && onMove(task.id, -1); } }} >
); }; /* ── DropSlot ─────────────────────────────────────────────────────────────── */ const DropSlot = ({ active }) => (