Menu
Public repository / a986cdcc29b5f2c7d03f819e1e5541d4751a1ea2
AkurAI-CRM / site/frontend/databinder.js
Latest change 564c7ee0b5787e8582e1da66b0a35d0a3597acfc - fix: round 3 comprehensive audit fixes by Ólafur B. J.
// 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 [];
}
}
export async function loadStats() {
const [people, companies, opportunities, tasks] = await Promise.all([
client.list('people').catch(() => []),
client.list('companies').catch(() => []),
client.list('opportunities').catch(() => []),
client.list('tasks').catch(() => []),
]);
setText('stat-contacts', (people.length + companies.length).toString());
setText('stat-deals', opportunities.filter(o => o.stage !== 'won' && o.stage !== 'lost').length.toString());
setText('stat-revenue', '$' + opportunities.filter(o => o.stage !== 'lost').reduce((s, o) => s + ((o.amount || 0) / 100), 0).toLocaleString());
setText('stat-tasks', tasks.filter(t => t.status !== 'done').length.toString());
}
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 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) {
if (!personId) return;
try {
const p = await client.get('people', personId);
setText('detail-name', `${p.firstName || ''} ${p.lastName || ''}`);
document.title = `${p.firstName || ''} ${p.lastName || ''} — AkurAI-CRM`;
setText('detail-jobtitle', p.jobTitle || '—');
setText('detail-email', p.email || '—');
setText('detail-phone', p.phone || '—');
setText('detail-company', p.companyId ? `Company #${p.companyId}` : '—');
if (p.firstName) {
const avatar = document.querySelector('#detail-avatar');
if (avatar) avatar.textContent = (p.firstName[0] || '') + (p.lastName?.[0] || '');
}
} catch(e) {
console.warn('Failed to load person detail:', e);
}
}
// Render company detail page
export async function loadCompanyDetail(companyId) {
if (!companyId) return;
try {
const c = await client.get('companies', companyId);
setText('detail-company-name', c.name || 'Unnamed');
setText('detail-domain', c.domainName || '—');
setText('detail-revenue', c.annualRevenue ? '$' + (c.annualRevenue / 100).toLocaleString() : '—');
setText('detail-employees', c.employeeCount?.toString() || '—');
const wsEl = document.getElementById('detail-website');
if (wsEl) {
wsEl.textContent = c.websiteUrl || '—';
wsEl.href = c.websiteUrl || '#';
}
// Load people at this company
const people = await client.list('people');
const employees = people.filter(p => p.companyId == companyId);
const tbody = document.getElementById('company-people-tbody');
if (tbody) {
tbody.innerHTML = employees.map(p => `
<tr>
<td><a href="/person-detail.html?id=${p.id}">${p.firstName || ''} ${p.lastName || ''}</a></td>
<td class="cell-muted">${p.email || '—'}</td>
<td class="cell-muted">${p.jobTitle || '—'}</td>
</tr>
`).join('') || '<tr><td colspan="3" class="empty-state">No contacts at this company</td></tr>';
}
} catch(e) {
console.warn('Failed to load company detail:', e);
}
}
// Render opportunity detail page
export async function loadOpportunityDetail(oppId) {
if (!oppId) return;
try {
const o = await client.get('opportunities', oppId);
setText('detail-deal-name', o.name || 'Unnamed');
setText('detail-deal-amount', o.amount ? '$' + (o.amount / 100).toLocaleString() : '—');
setText('detail-deal-stage', o.stage || '—');
setText('detail-deal-probability', o.probability != null ? o.probability + '%' : '—');
setText('detail-deal-person', o.personId ? `Person #${o.personId}` : '—');
setText('detail-deal-company', o.companyId ? `Company #${o.companyId}` : '—');
} catch(e) {
console.warn('Failed to load opportunity detail:', e);
}
}
// 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();
}