Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changeset/new-meals-battle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
229 changes: 229 additions & 0 deletions server/public/admin-org-detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,41 @@
margin-top: var(--space-1);
}

/* Engagement signals */
.signal-item {
display: flex;
justify-content: space-between;
padding: var(--space-2) 0;
border-bottom: var(--border-1) solid var(--color-gray-100);
font-size: var(--text-sm);
}
.signal-item:last-child {
border-bottom: none;
}
.signal-label {
color: var(--color-text-secondary);
}
.signal-value {
font-weight: var(--font-medium);
}
.signal-value.positive {
color: var(--color-success-600);
}
.signal-value.negative {
color: var(--color-text-muted);
}
.interest-badge {
display: inline-block;
padding: var(--space-1) var(--space-2);
border-radius: var(--radius-sm);
font-size: var(--text-xs);
font-weight: var(--font-semibold);
}
.interest-low { background: var(--color-gray-200); color: var(--color-text-secondary); }
.interest-medium { background: var(--color-warning-100); color: var(--color-warning-700); }
.interest-high { background: var(--color-success-100); color: var(--color-success-700); }
.interest-very_high { background: var(--color-success-200); color: var(--color-success-800); }

/* Grid layout */
.grid-2 {
display: grid;
Expand Down Expand Up @@ -498,6 +533,40 @@ <h1>
<div class="grid-2">
<!-- Left Column -->
<div>
<!-- Engagement Signals -->
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-4);">
<h2 style="margin-bottom: 0;">Engagement Signals</h2>
<button class="btn btn-secondary btn-sm" onclick="openInterestLevelModal()">Set Interest</button>
</div>
<div id="engagementSignalsContent">
<div class="signal-item">
<span class="signal-label">Interest Level</span>
<span class="signal-value" id="signalInterestLevel">-</span>
</div>
<div class="signal-item">
<span class="signal-label">Member Profile</span>
<span class="signal-value" id="signalMemberProfile">-</span>
</div>
<div class="signal-item">
<span class="signal-label">Dashboard Logins (30d)</span>
<span class="signal-value" id="signalLoginCount">-</span>
</div>
<div class="signal-item">
<span class="signal-label">Last Login</span>
<span class="signal-value" id="signalLastLogin">-</span>
</div>
<div class="signal-item">
<span class="signal-label">Working Groups</span>
<span class="signal-value" id="signalWorkingGroups">-</span>
</div>
<div class="signal-item">
<span class="signal-label">Email Clicks (30d)</span>
<span class="signal-value" id="signalEmailClicks">-</span>
</div>
</div>
</div>

<!-- Next Steps -->
<div class="card">
<h2>Pending Next Steps</h2>
Expand Down Expand Up @@ -720,6 +789,38 @@ <h3 style="font-size: var(--text-base); margin-bottom: var(--space-4);">Next Ste
</div>
</div>

<!-- Interest Level Modal -->
<div id="interestLevelModal" class="modal">
<div class="modal-content" style="max-width: 450px;">
<div class="modal-header">
<h2>Set Interest Level</h2>
<button class="modal-close" onclick="closeInterestLevelModal()">&times;</button>
</div>
<form id="interestLevelForm" onsubmit="saveInterestLevel(event)">
<div class="form-group">
<label>Interest Level</label>
<select id="interestLevelSelect">
<option value="">Not set</option>
<option value="low">Low - Not a good fit / not interested</option>
<option value="medium">Medium - Lukewarm / maybe later</option>
<option value="high">High - Interested / actively engaged</option>
<option value="very_high">Very High - Ready to move forward</option>
</select>
</div>

<div class="form-group">
<label>Note (optional)</label>
<textarea id="interestLevelNote" placeholder="e.g., Per conversation with CEO on 12/28..." rows="3"></textarea>
</div>

<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="closeInterestLevelModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</div>

<script>
let orgData = null;
let stakeholders = [];
Expand Down Expand Up @@ -779,6 +880,9 @@ <h3 style="font-size: var(--text-base); margin-bottom: var(--space-4);">Next Ste
document.getElementById('engagementReasons').textContent =
orgData.engagement_reasons?.join(' • ') || 'Cold';

// Render engagement signals
renderEngagementSignals(orgData.engagement_signals);

// Status
const status = orgData.prospect_status || 'signed_up';
document.getElementById('orgStatus').innerHTML =
Expand Down Expand Up @@ -1217,6 +1321,131 @@ <h3 style="font-size: var(--text-base); margin-bottom: var(--space-4);">Next Ste
}
}

// ========================================
// ENGAGEMENT SIGNALS
// ========================================

function renderEngagementSignals(signals) {
if (!signals) return;

// Interest level
const interestEl = document.getElementById('signalInterestLevel');
if (signals.interest_level) {
const levelLabels = {
'low': 'Low',
'medium': 'Medium',
'high': 'High',
'very_high': 'Very High'
};
let html = `<span class="interest-badge interest-${signals.interest_level}">${levelLabels[signals.interest_level]}</span>`;
if (signals.interest_level_set_by) {
html += ` <span style="color: var(--color-text-muted); font-size: var(--text-xs);">by ${signals.interest_level_set_by}</span>`;
}
if (signals.interest_level_set_at) {
const setDate = new Date(signals.interest_level_set_at);
html += ` <span style="color: var(--color-text-muted); font-size: var(--text-xs);">(${setDate.toLocaleDateString()})</span>`;
}
interestEl.innerHTML = html;
} else {
interestEl.innerHTML = '<span class="negative">Not set</span>';
}

// Member profile
const profileEl = document.getElementById('signalMemberProfile');
profileEl.innerHTML = signals.has_member_profile
? '<span class="positive">Configured</span>'
: '<span class="negative">Not configured</span>';

// Login count
const loginEl = document.getElementById('signalLoginCount');
loginEl.innerHTML = signals.login_count_30d > 0
? `<span class="positive">${signals.login_count_30d}</span>`
: '<span class="negative">0</span>';

// Last login
const lastLoginEl = document.getElementById('signalLastLogin');
if (signals.last_login) {
const lastLogin = new Date(signals.last_login);
lastLoginEl.textContent = lastLogin.toLocaleDateString();
} else {
lastLoginEl.innerHTML = '<span class="negative">Never</span>';
}

// Working groups
const wgEl = document.getElementById('signalWorkingGroups');
wgEl.innerHTML = signals.working_group_count > 0
? `<span class="positive">${signals.working_group_count}</span>`
: '<span class="negative">0</span>';

// Email clicks
const emailEl = document.getElementById('signalEmailClicks');
emailEl.innerHTML = signals.email_click_count_30d > 0
? `<span class="positive">${signals.email_click_count_30d}</span>`
: '<span class="negative">0</span>';
}

// Interest Level Modal
function openInterestLevelModal() {
const modal = document.getElementById('interestLevelModal');
const select = document.getElementById('interestLevelSelect');
const note = document.getElementById('interestLevelNote');

// Pre-populate with current values
if (orgData.engagement_signals) {
select.value = orgData.engagement_signals.interest_level || '';
note.value = orgData.engagement_signals.interest_level_note || '';
}

modal.style.display = 'flex';
}

function closeInterestLevelModal() {
document.getElementById('interestLevelModal').style.display = 'none';
}

async function saveInterestLevel(event) {
event.preventDefault();

const interest_level = document.getElementById('interestLevelSelect').value || null;
const note = document.getElementById('interestLevelNote').value.trim() || null;

try {
const response = await fetch(`/api/admin/organizations/${orgId}/interest-level`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ interest_level, note })
});

if (!response.ok) {
if (response.status === 401) {
window.AdminNav.redirectToLogin();
return;
}
throw new Error('Failed to save interest level');
}

const data = await response.json();

// Update the local data and re-render
orgData.engagement_signals = data.engagement_signals;
renderEngagementSignals(data.engagement_signals);

// Re-fetch the full org data to get updated engagement level/reasons
const orgResponse = await fetch(`/api/admin/organizations/${orgId}`);
if (orgResponse.ok) {
orgData = await orgResponse.json();
const fires = '🔥'.repeat(orgData.engagement_level || 1);
document.getElementById('engagementFires').textContent = fires;
document.getElementById('engagementReasons').textContent =
orgData.engagement_reasons?.join(' • ') || 'Cold';
}

closeInterestLevelModal();
} catch (error) {
alert('Error: ' + error.message);
}
}

// Close modal when clicking outside
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
Expand Down
19 changes: 19 additions & 0 deletions server/src/db/migrations/039_member_engagement.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- Migration: 039_member_engagement.sql
-- Add interest level fields for human-set engagement assessment
-- Login tracking uses existing org_activities table with activity_type = 'dashboard_login'

-- Add interest level fields to organizations
-- Human-set interest level with attribution (e.g., "high (as of 11/30/25 per Brian)")
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS interest_level VARCHAR(50); -- low, medium, high, very_high
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS interest_level_note TEXT; -- Free text note about the interest level
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS interest_level_set_by VARCHAR(255); -- Name of person who set it
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS interest_level_set_at TIMESTAMP WITH TIME ZONE;

-- Index for filtering by activity type (useful for counting logins)
CREATE INDEX IF NOT EXISTS idx_org_activities_type ON org_activities(activity_type);

-- Comments
COMMENT ON COLUMN organizations.interest_level IS 'Human-set interest level: low, medium, high, very_high';
COMMENT ON COLUMN organizations.interest_level_note IS 'Free text note about the interest level assessment';
COMMENT ON COLUMN organizations.interest_level_set_by IS 'Name of the person who set the interest level';
COMMENT ON COLUMN organizations.interest_level_set_at IS 'When the interest level was last set';
Loading