AkurAI Build
Menu

Public repository / 228b5716ba0b7bcc87cce261ea6df891133a35bb

AkurAI-CRM / site/frontend/databinder.js

Latest change 95d0fbb40c7a5cfcad35aff736d002228c61a48c - AKURAI-CRM-4 render dashboard from live data by Ólafur Búi Ólafsson

// databinder.js — Client-side data binding for CRM pages
// Fetches from API and renders into DOM

import { client } from './crm.js';

export async function loadTable(entity, tableBodyId) {
  try {
    const data = await client.list(entity);
    const tbody = document.getElementById(tableBodyId);
    if (!tbody || !data || !data.length) {
      if (tbody) tbody.innerHTML = '<tr><td colspan="10" class="empty-state">No records found</td></tr>';
      return data || [];
    }
    return data;
  } catch (e) {
    console.warn(`Failed to load ${entity}:`, e);
    const tbody = document.getElementById(tableBodyId);
    if (tbody) tbody.innerHTML = `<tr><td colspan="10" class="empty-state">Error loading ${entity}</td></tr>`;
    return [];
  }
}

function dashboardState(id, message, type = 'loading') {
  const target = document.getElementById(id);
  if (!target) return;
  const baseClass = target.dataset.baseClass || target.className;
  target.dataset.baseClass = baseClass;
  target.className = type === 'loading' ? baseClass : `${baseClass} empty-state dashboard-state-${type}`;
  target.textContent = message;
}

function renderDashboardPipeline(opportunities) {
  const target = document.getElementById('dashboard-pipeline');
  if (!target) return;
  const stages = ['new', 'screening', 'meeting', 'proposal', 'negotiation', 'won', 'lost'];
  const labels = { new: 'New', screening: 'Screening', meeting: 'Meeting', proposal: 'Proposal', negotiation: 'Negotiation', won: 'Won', lost: 'Lost' };
  const totals = Object.fromEntries(stages.map(stage => [stage, opportunities.filter(item => (item.stage || 'new') === stage).reduce((sum, item) => sum + (item.amount || 0), 0)]));
  const max = Math.max(...Object.values(totals), 1);
  if (!opportunities.length) {
    dashboardState('dashboard-pipeline', 'No opportunities recorded.', 'empty');
    return;
  }
  target.innerHTML = stages.map(stage => `
    <div class="pipeline-bar">
      <div class="bar" style="height:${Math.max(8, Math.round(totals[stage] / max * 100))}px;background:${getStageColor(stage)};"></div>
      <span class="bar-value">${totals[stage] ? '$' + (totals[stage] / 100).toLocaleString() : '—'}</span>
      <span class="bar-label">${labels[stage]}</span>
    </div>`).join('');
}

function renderDashboardActivity(people, companies, opportunities, tasks) {
  const target = document.getElementById('dashboard-activity');
  if (!target) return;
  const events = [
    ...people.map(item => ({ date: item.createdAt, text: `${item.name || `${item.firstName || ''} ${item.lastName || ''}`.trim() || 'Person'} was added as a contact` })),
    ...companies.map(item => ({ date: item.createdAt, text: `${item.name || 'Company'} was added as a company` })),
    ...opportunities.map(item => ({ date: item.updatedAt || item.createdAt, text: `${item.name || 'Opportunity'} is in ${item.stage || 'new'}` })),
    ...tasks.filter(item => item.status === 'done').map(item => ({ date: item.updatedAt || item.createdAt, text: `Task ${item.title || 'Untitled'} was completed` })),
  ].filter(event => event.date).sort((a, b) => b.date - a.date).slice(0, 8);
  if (!events.length) {
    dashboardState('dashboard-activity', 'No activity recorded.', 'empty');
    return;
  }
  target.innerHTML = events.map(event => `<div class="feed-item"><div class="feed-content"><div class="feed-text">${escapeHtml(event.text)}</div><div class="feed-meta">${escapeHtml(formatDetailDate(event.date))}</div></div></div>`).join('');
}

function renderDashboardTasks(tasks) {
  const target = document.getElementById('dashboard-tasks');
  if (!target) return;
  const upcoming = tasks.filter(item => item.status !== 'done').sort((a, b) => (a.dueAt || Infinity) - (b.dueAt || Infinity)).slice(0, 6);
  if (!upcoming.length) {
    dashboardState('dashboard-tasks', 'No upcoming tasks.', 'empty');
    return;
  }
  target.innerHTML = upcoming.map(task => `<div class="task-item"><span class="task-title">${escapeHtml(task.title || 'Untitled task')}</span><div class="task-meta"><span class="task-due">${escapeHtml(task.dueAt ? formatDetailDate(task.dueAt) : '—')}</span><span class="badge badge-blue">${escapeHtml(task.status || 'todo')}</span></div></div>`).join('');
}

function renderDashboardQuickStats(opportunities, people) {
  const target = document.getElementById('dashboard-quick-stats');
  if (!target) return;
  const average = opportunities.length ? opportunities.reduce((sum, item) => sum + (item.amount || 0), 0) / opportunities.length : 0;
  const decided = opportunities.filter(item => ['won', 'lost'].includes(item.stage));
  const winRate = decided.length ? (decided.filter(item => item.stage === 'won').length / decided.length) * 100 : 0;
  const cycles = opportunities.filter(item => item.createdAt && item.closeDate).map(item => item.closeDate - item.createdAt).filter(days => days >= 0);
  const averageCycle = cycles.length ? cycles.reduce((sum, days) => sum + days, 0) / cycles.length / 86400 : 0;
  target.innerHTML = `<div style="display:flex;flex-direction:column;gap:var(--space-4);">
    <div style="display:flex;justify-content:space-between;align-items:center;"><span class="cell-muted">Avg. Deal Size</span><strong>${average ? '$' + (average / 100).toLocaleString() : '—'}</strong></div>
    <div style="display:flex;justify-content:space-between;align-items:center;"><span class="cell-muted">Win Rate</span><strong>${decided.length ? Math.round(winRate) + '%' : '—'}</strong></div>
    <div style="display:flex;justify-content:space-between;align-items:center;"><span class="cell-muted">Avg. Sales Cycle</span><strong>${averageCycle ? Math.round(averageCycle) + ' days' : '—'}</strong></div>
    <div style="display:flex;justify-content:space-between;align-items:center;"><span class="cell-muted">Active Contacts</span><strong>${people.length}</strong></div>
  </div>`;
}

export async function loadStats() {
  dashboardState('dashboard-pipeline', 'Loading pipeline…');
  dashboardState('dashboard-activity', 'Loading activity…');
  dashboardState('dashboard-tasks', 'Loading tasks…');
  dashboardState('dashboard-quick-stats', 'Loading stats…');
  try {
    const [people, companies, opportunities, tasks] = await Promise.all([
      client.list('people'),
      client.list('companies'),
      client.list('opportunities'),
      client.list('tasks'),
    ]);
    const activeDeals = opportunities.filter(item => !['won', 'lost'].includes(item.stage));
    const revenue = opportunities.filter(item => item.stage !== 'lost').reduce((sum, item) => sum + (item.amount || 0), 0);
    setText('stat-contacts', (people.length + companies.length).toString());
    setText('stat-deals', activeDeals.length.toString());
    setText('stat-revenue', '$' + (revenue / 100).toLocaleString());
    setText('stat-tasks', tasks.filter(item => item.status !== 'done').length.toString());
    setText('stat-contacts-side', people.length.toString());
    renderDashboardPipeline(opportunities);
    renderDashboardActivity(people, companies, opportunities, tasks);
    renderDashboardTasks(tasks);
    renderDashboardQuickStats(opportunities, people);
  } catch (error) {
    console.warn('Failed to load dashboard:', error);
    ['dashboard-pipeline', 'dashboard-activity', 'dashboard-tasks', 'dashboard-quick-stats'].forEach(id => dashboardState(id, 'Unable to load dashboard data.', 'error'));
    ['stat-contacts', 'stat-deals', 'stat-revenue', 'stat-tasks'].forEach(id => setText(id, '—'));
  }
}

export function renderPeopleTable(data, tbodyId) {
  const tbody = document.getElementById(tbodyId);
  if (!tbody) return;
  tbody.innerHTML = data.map(p => `
    <tr>
      <td><a href="/person-detail.html?id=${p.id}" class="person-cell">
        <div class="avatar avatar-sm">${(p.firstName?.[0]||'')}${(p.lastName?.[0]||'')}</div>
        ${p.firstName || ''} ${p.lastName || ''}
      </a></td>
      <td class="cell-muted">${p.email || '—'}</td>
      <td class="cell-muted">${p.phone || '—'}</td>
      <td class="cell-muted">${p.jobTitle || '—'}</td>
      <td class="cell-muted">${p.companyId ? `<a href="/company-detail.html?id=${p.companyId}">Company #${p.companyId}</a>` : '—'}</td>
    </tr>
  `).join('');
  updatePagination(tbodyId, data.length);
}

export function renderCompaniesTable(data, tbodyId) {
  const tbody = document.getElementById(tbodyId);
  if (!tbody) return;
  tbody.innerHTML = data.map(c => `
    <tr>
      <td><a href="/company-detail.html?id=${c.id}" class="person-cell">${c.name || 'Unnamed'}</a></td>
      <td class="cell-muted">${c.domainName || '—'}</td>
      <td class="cell-muted">${c.annualRevenue ? '$' + (c.annualRevenue / 100).toLocaleString() : '—'}</td>
      <td class="cell-muted">${c.employeeCount || '—'}</td>
      <td class="cell-muted">${c.websiteUrl ? `<a href="${c.websiteUrl}" target="_blank">${c.websiteUrl}</a>` : '—'}</td>
    </tr>
  `).join('');
  updatePagination(tbodyId, data.length);
}

export function renderOpportunitiesTable(data, tbodyId) {
  const tbody = document.getElementById(tbodyId);
  if (!tbody) return;
  tbody.innerHTML = data.map(o => {
    const stageClass = `badge badge-${o.stage || 'new'}`;
    return `<tr>
      <td><a href="/opportunity-detail.html?id=${o.id}">${o.name || 'Unnamed'}</a></td>
      <td class="cell-muted">${o.amount ? '$' + (o.amount / 100).toLocaleString() : '—'}</td>
      <td><span class="${stageClass}">${o.stage || 'new'}</span></td>
      <td class="cell-muted">${o.probability != null ? o.probability + '%' : '—'}</td>
      <td class="cell-muted">${o.personId ? `Person #${o.personId}` : '—'}</td>
    </tr>`;
  }).join('');
  updatePagination(tbodyId, data.length);
}

export function renderTasksList(data, listId) {
  const list = document.getElementById(listId);
  if (!list) return;
  list.innerHTML = data.map(t => {
    const isDone = t.status === 'done';
    return `<div class="task-item${isDone ? ' done' : ''}" data-status="${t.status || 'todo'}">
      <input type="checkbox" class="task-checkbox"${isDone ? ' checked' : ''}>
      <span class="task-title">${t.title || 'Untitled'}</span>
      <div class="task-meta">
        <span class="task-due">${t.dueDate || '—'}</span>
        <span class="badge badge-${isDone ? 'green' : 'blue'}">${t.relatedTo || t.status || 'todo'}</span>
      </div>
    </div>`;
  }).join('');
  updatePagination(listId, data.length);
}

function setText(id, text) {
  const el = document.getElementById(id);
  if (el) el.textContent = text;
}
function escapeHtml(value) {
  return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
}

function formatDetailDate(value) {
  if (!value) return '—';
  const date = new Date(Number(value) * 1000);
  return Number.isNaN(date.getTime()) ? '—' : date.toLocaleString();
}

function setState(message, type = 'loading') {
  const state = document.getElementById('detail-state');
  if (!state) return;
  state.textContent = message;
  state.className = `empty-state detail-state-${type}`;
  state.hidden = !message;
}

function setLink(id, text, href) {
  const link = document.getElementById(id);
  if (!link) return;
  link.textContent = text || '—';
  if (href) {
    link.href = href;
    link.hidden = false;
  } else {
    link.removeAttribute('href');
    link.hidden = true;
  }
}

function related(record, field, id) {
  return Array.isArray(record?.[field]) && record[field].some(value => String(value) === String(id));
}

function renderTimeline(id, timeline) {
  const target = document.getElementById(id);
  if (!target) return;
  if (!timeline?.length) {
    target.innerHTML = '<div class="empty-state">No activity recorded.</div>';
    return;
  }
  target.innerHTML = timeline.map(event => `
    <div class="timeline-item">
      <div class="timeline-dot"></div>
      <div class="timeline-content">
        <div class="timeline-title">${escapeHtml(event.action || 'Activity')}</div>
        <div class="timeline-meta">${escapeHtml(formatDetailDate(event.createdAt))}</div>
      </div>
    </div>`).join('');
}

function renderRelatedTasks(id, tasks) {
  const target = document.getElementById(id);
  if (!target) return;
  if (!tasks?.length) {
    target.innerHTML = '<div class="empty-state">No tasks recorded.</div>';
    return;
  }
  target.innerHTML = tasks.map(task => `
    <div class="task-item${task.status === 'done' ? ' done' : ''}">
      <span class="task-title">${escapeHtml(task.title || 'Untitled task')}</span>
      <div class="task-meta">
        <span class="task-due">${escapeHtml(task.dueAt ? formatDetailDate(task.dueAt) : '—')}</span>
        <span class="badge badge-${task.status === 'done' ? 'green' : 'blue'}">${escapeHtml(task.status || 'todo')}</span>
      </div>
    </div>`).join('');
}

function renderRelatedNotes(id, notes) {
  const target = document.getElementById(id);
  if (!target) return;
  if (!notes?.length) {
    target.innerHTML = '<div class="empty-state">No notes recorded.</div>';
    return;
  }
  target.innerHTML = notes.map(note => `
    <article class="card">
      <div class="note-card-header">
        <h3 class="note-card-title">${escapeHtml(note.title || 'Untitled note')}</h3>
        <span class="text-xs text-muted">${escapeHtml(formatDetailDate(note.createdAt))}</span>
      </div>
      <p class="note-card-preview">${escapeHtml(note.body || '')}</p>
    </article>`).join('');
}

function renderRelatedOpportunities(id, opportunities) {
  const target = document.getElementById(id);
  if (!target) return;
  const open = (opportunities || []).filter(opportunity => !['won', 'lost'].includes(opportunity.stage));
  if (!open.length) {
    target.innerHTML = '<div class="empty-state">No open opportunities.</div>';
    return;
  }
  target.className = '';
  target.innerHTML = open.map(opportunity => `
    <div style="display:flex;align-items:center;justify-content:space-between;padding:var(--space-2) 0;border-top:1px solid var(--color-border-light);">
      <a href="/opportunity-detail.html?id=${encodeURIComponent(opportunity.id)}">${escapeHtml(opportunity.name || 'Unnamed opportunity')}</a>
      <span class="badge badge-${escapeHtml(opportunity.stage || 'new')}">${escapeHtml(opportunity.stage || 'new')}</span>
    </div>`).join('');
}

function updatePagination(tbodyId, total) {
  const parent = document.getElementById(tbodyId)?.closest('.card') || document;
  const pagination = parent.querySelector('.pagination');
  if (pagination) {
    const info = pagination.querySelector('.pagination-info');
    if (info) info.textContent = `Showing all ${total} records`;
  }
}

// Render person detail page
export async function loadPersonDetail(personId) {
  setState('Loading person…');
  if (!/^\d+$/.test(String(personId || ''))) {
    setState('Select a person to view.', 'error');
    return;
  }
  try {
    const [person, companies, opportunities, tasks, notes, timeline] = await Promise.all([
      client.get('people', personId),
      client.list('companies'),
      client.list('opportunities'),
      client.list('tasks'),
      client.list('notes'),
      client.timeline('person', personId),
    ]);
    const company = companies.find(item => String(item.id) === String(person.companyId));
    const name = person.name || `${person.firstName || ''} ${person.lastName || ''}`.trim() || 'Unnamed person';
    setText('detail-name', name);
    setText('breadcrumb-name', name);
    document.title = `${name} — AkurAI-CRM`;
    setText('detail-jobtitle', person.jobTitle || '—');
    setText('detail-jobtitle-secondary', person.jobTitle || '—');
    setText('detail-email', person.email || '—');
    setText('detail-phone', person.phone || '—');
    setText('detail-address', person.address || '—');
    setText('detail-linkedin', person.linkedinUrl || '—');
    setText('detail-company', company?.name || '—');
    setLink('detail-company-link', company?.name || '—', company ? `/company-detail.html?id=${company.id}` : '');
    setText('detail-department', person.department || '—');
    setText('detail-reports-to', person.reportsTo || '—');
    setText('detail-status', 'Person');
    setText('detail-avatar', `${person.firstName?.[0] || ''}${person.lastName?.[0] || ''}` || '—');
    renderRelatedOpportunities('person-opportunities', opportunities.filter(item => String(item.personId) === String(personId)));
    renderRelatedTasks('person-tasks', tasks.filter(item => related(item, 'personIds', personId)));
    renderRelatedNotes('person-notes', notes.filter(item => related(item, 'personIds', personId)));
    renderTimeline('person-timeline', timeline.timeline || []);
    setState('');
  } catch (error) {
    console.warn('Failed to load person detail:', error);
    setState('Unable to load this person. Try again later.', 'error');
  }
}

// Render company detail page
export async function loadCompanyDetail(companyId) {
  setState('Loading company…');
  if (!/^\d+$/.test(String(companyId || ''))) {
    setState('Select a company to view.', 'error');
    return;
  }
  try {
    const [company, people, opportunities, timeline] = await Promise.all([
      client.get('companies', companyId),
      client.list('people'),
      client.list('opportunities'),
      client.timeline('company', companyId),
    ]);
    const name = company.name || 'Unnamed company';
    setText('detail-company-name', name);
    setText('breadcrumb-name', name);
    document.title = `${name} — AkurAI-CRM`;
    setText('detail-domain', company.domainName || '—');
    setText('detail-domain-info', company.domainName || '—');
    setText('detail-revenue', company.annualRevenue ? '$' + (company.annualRevenue / 100).toLocaleString() : '—');
    setText('detail-employees', company.employeeCount?.toString() || '—');
    setText('detail-address', company.address || '—');
    setText('detail-phone', company.phone || '—');
    setText('detail-industry', company.industry || '—');
    setText('detail-description', company.description || 'No description provided.');
    setText('detail-status', 'Company');
    setLink('detail-website', company.websiteUrl || '—', company.websiteUrl);
    const employees = people.filter(person => String(person.companyId) === String(companyId));
    const tbody = document.getElementById('company-people-tbody');
    if (tbody) {
      tbody.innerHTML = employees.length
        ? employees.map(person => `<tr><td><a href="/person-detail.html?id=${encodeURIComponent(person.id)}">${escapeHtml(person.name || `${person.firstName || ''} ${person.lastName || ''}`)}</a></td><td class="cell-muted">${escapeHtml(person.email || '—')}</td><td class="cell-muted">${escapeHtml(person.jobTitle || '—')}</td></tr>`).join('')
        : '<tr><td colspan="3" class="empty-state">No contacts at this company.</td></tr>';
    }
    renderRelatedOpportunities('company-opportunities', opportunities.filter(item => String(item.companyId) === String(companyId)));
    renderTimeline('company-timeline', timeline.timeline || []);
    setText('tab-people-count', employees.length);
    setText('tab-opportunities-count', opportunities.filter(item => String(item.companyId) === String(companyId)).length);
    setState('');
  } catch (error) {
    console.warn('Failed to load company detail:', error);
    setState('Unable to load this company. Try again later.', 'error');
  }
}

// Render opportunity detail page
export async function loadOpportunityDetail(oppId) {
  setState('Loading opportunity…');
  if (!/^\d+$/.test(String(oppId || ''))) {
    setState('Select an opportunity to view.', 'error');
    return;
  }
  try {
    const [opportunity, people, companies, tasks, timeline] = await Promise.all([
      client.get('opportunities', oppId),
      client.list('people'),
      client.list('companies'),
      client.list('tasks'),
      client.timeline('opportunity', oppId),
    ]);
    const stage = opportunity.stage || 'new';
    const stageLabels = { new: 'New', screening: 'Screening', meeting: 'Meeting', proposal: 'Proposal', negotiation: 'Negotiation', won: 'Won', lost: 'Lost' };
    const stages = Object.keys(stageLabels);
    const stageIndex = stages.indexOf(stage);
    const closeDate = opportunity.closeDate ? formatDetailDate(opportunity.closeDate) : '—';
    const person = people.find(item => String(item.id) === String(opportunity.personId));
    const company = companies.find(item => String(item.id) === String(opportunity.companyId));
    setText('detail-deal-name', opportunity.name || 'Unnamed opportunity');
    setText('breadcrumb-name', opportunity.name || 'Opportunity');
    document.title = `${opportunity.name || 'Opportunity'} — AkurAI-CRM`;
    setText('detail-deal-stage', stageLabels[stage] || stage);
    setText('detail-deal-close-date', closeDate);
    setText('detail-deal-close-date-info', closeDate);
    setText('detail-deal-amount', opportunity.amount ? '$' + (opportunity.amount / 100).toLocaleString() : '—');
    setText('detail-deal-probability', opportunity.probability != null ? `${opportunity.probability}%` : '—');
    setText('detail-deal-expected-value', opportunity.amount && opportunity.probability != null ? '$' + ((opportunity.amount * opportunity.probability / 100) / 100).toLocaleString() : '—');
    setLink('detail-deal-person', person?.name || '—', person ? `/person-detail.html?id=${person.id}` : '');
    setLink('detail-deal-company', company?.name || '—', company ? `/company-detail.html?id=${company.id}` : '');
    setText('detail-deal-type', opportunity.dealType || '—');
    setText('detail-deal-description', opportunity.description || 'No description provided.');
    setText('detail-deal-tasks-heading', 'Tasks');
    const stageTarget = document.getElementById('detail-stage-progress');
    if (stageTarget) {
      stageTarget.innerHTML = stages.map((label, index) => `<div class="stage-step${index < stageIndex ? ' completed' : ''}${index === stageIndex ? ' active' : ''}"><div class="stage-circle${index < stageIndex ? ' completed' : ''}${index === stageIndex ? ' active' : ''}">${index < stageIndex ? '✓' : index + 1}</div><div class="stage-label">${stageLabels[label]}</div></div>${index < stages.length - 1 ? `<div class="stage-connector${index < stageIndex ? ' completed' : ''}"></div>` : ''}`).join('');
    }
    renderRelatedTasks('detail-deal-tasks', tasks.filter(item => related(item, 'opportunityIds', oppId)));
    renderTimeline('detail-deal-timeline', timeline.timeline || []);
    setState('');
  } catch (error) {
    console.warn('Failed to load opportunity detail:', error);
    setState('Unable to load this opportunity. Try again later.', 'error');
  }
}

// Render notes grid
export async function loadNotes() {
  try {
    const notes = await client.list('notes');
    const grid = document.getElementById('notes-grid');
    if (!grid) return;
    grid.innerHTML = notes.map(n => `
      <div class="card note-card">
        <div class="note-card-header">
          <h3 class="note-card-title">${n.title || 'Untitled Note'}</h3>
          <span class="text-xs text-muted">${new Date((n.createdAt || 0) * 1000).toLocaleDateString()}</span>
        </div>
        <div class="note-card-preview">${(n.body || '').substring(0, 120)}${(n.body || '').length > 120 ? '...' : ''}</div>
      </div>
    `).join('');
  } catch(e) {
    console.warn('Failed to load notes:', e);
  }
}

// Render kanban board from API data
export async function loadKanban() {
  try {
    const opps = await client.list('opportunities');
    const stages = ['new', 'screening', 'meeting', 'proposal', 'negotiation', 'won', 'lost'];
    const stageLabels = { new: 'New', screening: 'Screening', meeting: 'Meeting', proposal: 'Proposal', negotiation: 'Negotiation', won: 'Won', lost: 'Lost' };

    stages.forEach(stage => {
      const col = document.getElementById(`kanban-${stage}`);
      if (!col) return;
      const cards = opps.filter(o => (o.stage || 'new') === stage);
      col.innerHTML = cards.map(o => `
        <a href="/opportunity-detail.html?id=${o.id}" class="kanban-card" style="text-decoration:none;color:inherit;border-left-color: ${getStageColor(stage)};">
          <div class="card-title">${o.name || 'Unnamed'}</div>
          <div class="card-subtitle">${o.amount ? '$' + (o.amount / 100).toLocaleString() : '—'}</div>
          <div class="card-footer">
            <div class="card-amount">${o.amount ? '$' + (o.amount / 100).toLocaleString() : '—'}</div>
            <span class="badge badge-${stage}">${stageLabels[stage] || stage}</span>
          </div>
        </a>
      `).join('');

      // Update column header count
      const header = col.closest('.kanban-column')?.querySelector('.kanban-count');
      if (header) header.textContent = cards.length.toString();
    });
  } catch(e) {
    console.warn('Failed to load kanban:', e);
  }
}

function getStageColor(stage) {
  const colors = { new: '#94a3b8', screening: '#60a5fa', meeting: '#818cf8', proposal: '#a78bfa', negotiation: '#f59e0b', won: '#22c55e', lost: '#ef4444' };
  return colors[stage] || '#94a3b8';
}

// Auto-init: run stat loading on dashboard
if (window.location.pathname.includes('dashboard')) {
  loadStats();
}