|
174 | 174 | color: var(--color-text-muted); |
175 | 175 | margin-left: var(--space-2); |
176 | 176 | } |
| 177 | + .member-name-link { |
| 178 | + color: var(--color-brand); |
| 179 | + text-decoration: none; |
| 180 | + cursor: pointer; |
| 181 | + } |
| 182 | + .member-name-link:hover { |
| 183 | + text-decoration: underline; |
| 184 | + } |
177 | 185 |
|
178 | 186 | /* Working groups */ |
179 | 187 | .wg-badge { |
@@ -1009,6 +1017,19 @@ <h2>Add Domain</h2> |
1009 | 1017 | </div> |
1010 | 1018 | </div> |
1011 | 1019 |
|
| 1020 | + <!-- User Context Modal --> |
| 1021 | + <div id="userContextModal" class="modal"> |
| 1022 | + <div class="modal-content" style="max-width: 700px;"> |
| 1023 | + <div class="modal-header"> |
| 1024 | + <h2 id="userContextModalTitle">Member Context</h2> |
| 1025 | + <button class="modal-close" onclick="closeUserContextModal()">×</button> |
| 1026 | + </div> |
| 1027 | + <div id="userContextModalBody" style="padding: var(--space-4);"> |
| 1028 | + <!-- Content populated by JavaScript --> |
| 1029 | + </div> |
| 1030 | + </div> |
| 1031 | + </div> |
| 1032 | + |
1012 | 1033 | <script> |
1013 | 1034 | let orgData = null; |
1014 | 1035 | let stakeholders = []; |
@@ -1168,11 +1189,16 @@ <h2>Add Domain</h2> |
1168 | 1189 | count.textContent = `(${orgData.members.length})`; |
1169 | 1190 | list.innerHTML = orgData.members.map(member => { |
1170 | 1191 | const name = [member.firstName, member.lastName].filter(Boolean).join(' ') || member.email; |
| 1192 | + const escapedName = escapeHtml(name); |
| 1193 | + const escapedEmail = escapeHtml(member.email); |
| 1194 | + const memberId = member.id; |
1171 | 1195 | return ` |
1172 | 1196 | <li> |
1173 | | - ${name} |
1174 | | - <span class="member-role">${member.role}</span> |
1175 | | - <br><span style="font-size: var(--text-xs); color: var(--color-text-muted);">${member.email}</span> |
| 1197 | + ${memberId |
| 1198 | + ? `<a href="#" class="member-name-link" onclick="showUserContext('${escapeHtml(memberId)}', 'workos', '${escapedName}'); return false;">${escapedName}</a>` |
| 1199 | + : escapedName} |
| 1200 | + <span class="member-role">${escapeHtml(member.role)}</span> |
| 1201 | + <br><span style="font-size: var(--text-xs); color: var(--color-text-muted);">${escapedEmail}</span> |
1176 | 1202 | </li> |
1177 | 1203 | `; |
1178 | 1204 | }).join(''); |
@@ -1947,13 +1973,175 @@ <h2>Add Domain</h2> |
1947 | 1973 | } |
1948 | 1974 | } |
1949 | 1975 |
|
| 1976 | + // ======================================== |
| 1977 | + // USER CONTEXT MODAL |
| 1978 | + // ======================================== |
| 1979 | + |
| 1980 | + async function showUserContext(userId, type, userName) { |
| 1981 | + const modal = document.getElementById('userContextModal'); |
| 1982 | + const modalTitle = document.getElementById('userContextModalTitle'); |
| 1983 | + const modalBody = document.getElementById('userContextModalBody'); |
| 1984 | + |
| 1985 | + modalTitle.textContent = `Context: ${userName}`; |
| 1986 | + modalBody.innerHTML = '<div style="text-align: center; padding: var(--space-8);"><p>Loading context...</p></div>'; |
| 1987 | + modal.style.display = 'block'; |
| 1988 | + |
| 1989 | + try { |
| 1990 | + const response = await fetch(`/api/admin/users/${userId}/context?type=${type}`); |
| 1991 | + if (!response.ok) { |
| 1992 | + throw new Error('Failed to fetch context'); |
| 1993 | + } |
| 1994 | + const context = await response.json(); |
| 1995 | + modalBody.innerHTML = renderUserContext(context, userId); |
| 1996 | + } catch (error) { |
| 1997 | + console.error('Error fetching context:', error); |
| 1998 | + modalBody.innerHTML = ` |
| 1999 | + <div style="text-align: center; padding: var(--space-8); color: var(--color-error-600);"> |
| 2000 | + <p>Failed to load context.</p> |
| 2001 | + <button class="btn btn-secondary" onclick="showUserContext('${escapeHtml(userId)}', '${escapeHtml(type)}', '${escapeHtml(userName)}')">Retry</button> |
| 2002 | + </div> |
| 2003 | + `; |
| 2004 | + } |
| 2005 | + } |
| 2006 | + |
| 2007 | + function closeUserContextModal() { |
| 2008 | + document.getElementById('userContextModal').style.display = 'none'; |
| 2009 | + } |
| 2010 | + |
| 2011 | + function renderUserContext(context, userId) { |
| 2012 | + let html = ''; |
| 2013 | + |
| 2014 | + // Addie's Goal Section |
| 2015 | + if (context.addie_goal) { |
| 2016 | + html += '<div style="background: var(--color-primary-50); border-radius: var(--radius-md); padding: var(--space-4); margin-bottom: var(--space-4);">'; |
| 2017 | + html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Addie\'s Goal</h3>'; |
| 2018 | + html += `<div style="font-weight: var(--font-medium); color: var(--color-text-heading);">${escapeHtml(context.addie_goal.goal_name || context.addie_goal.goal_key)}</div>`; |
| 2019 | + if (context.addie_goal.reasoning) { |
| 2020 | + html += `<p style="font-size: var(--text-sm); color: var(--color-text-secondary); margin-top: var(--space-1);">${escapeHtml(context.addie_goal.reasoning)}</p>`; |
| 2021 | + } |
| 2022 | + html += '</div>'; |
| 2023 | + } |
| 2024 | + |
| 2025 | + // Identity Section |
| 2026 | + html += '<div style="margin-bottom: var(--space-4);">'; |
| 2027 | + html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Identity</h3>'; |
| 2028 | + html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-3);">'; |
| 2029 | + |
| 2030 | + if (context.workos_user) { |
| 2031 | + html += renderContextItem('Email', context.workos_user.email); |
| 2032 | + const name = `${context.workos_user.first_name || ''} ${context.workos_user.last_name || ''}`.trim(); |
| 2033 | + if (name) html += renderContextItem('Name', name); |
| 2034 | + } |
| 2035 | + |
| 2036 | + if (context.slack_user && !context.workos_user) { |
| 2037 | + html += renderContextItem('Email', context.slack_user.email || 'Not set'); |
| 2038 | + html += renderContextItem('Slack Name', context.slack_user.display_name || context.slack_user.real_name || 'Not set'); |
| 2039 | + } |
| 2040 | + |
| 2041 | + // Status badges |
| 2042 | + const statusItems = []; |
| 2043 | + if (context.is_member) statusItems.push('<span style="display: inline-block; padding: 2px 8px; background: var(--color-success-100); color: var(--color-success-700); border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: var(--font-medium);">Member</span>'); |
| 2044 | + if (context.slack_linked) statusItems.push('<span style="display: inline-block; padding: 2px 8px; background: #4A154B20; color: #4A154B; border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: var(--font-medium);">Slack</span>'); |
| 2045 | + |
| 2046 | + if (statusItems.length > 0) { |
| 2047 | + html += `<div style="background: var(--color-gray-50); padding: var(--space-3); border-radius: var(--radius-md); grid-column: 1 / -1;"> |
| 2048 | + <div style="font-size: var(--text-xs); color: var(--color-text-muted); margin-bottom: var(--space-1);">Status</div> |
| 2049 | + <div style="display: flex; gap: var(--space-1);">${statusItems.join(' ')}</div> |
| 2050 | + </div>`; |
| 2051 | + } |
| 2052 | + |
| 2053 | + html += '</div></div>'; |
| 2054 | + |
| 2055 | + // Organization Section |
| 2056 | + if (context.organization) { |
| 2057 | + html += '<div style="margin-bottom: var(--space-4);">'; |
| 2058 | + html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Organization</h3>'; |
| 2059 | + html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-3);">'; |
| 2060 | + html += renderContextItem('Company', context.organization.name); |
| 2061 | + const subStatus = context.organization.subscription_status || 'None'; |
| 2062 | + html += renderContextItem('Subscription', subStatus); |
| 2063 | + html += '</div></div>'; |
| 2064 | + } |
| 2065 | + |
| 2066 | + // Activity Section |
| 2067 | + html += '<div style="margin-bottom: var(--space-4);">'; |
| 2068 | + html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Activity</h3>'; |
| 2069 | + html += '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: var(--space-3);">'; |
| 2070 | + |
| 2071 | + if (context.engagement) { |
| 2072 | + html += renderContextItem('Dashboard Logins (30d)', context.engagement.login_count_30d.toString()); |
| 2073 | + if (context.engagement.last_login) { |
| 2074 | + html += renderContextItem('Last Login', new Date(context.engagement.last_login).toLocaleDateString()); |
| 2075 | + } |
| 2076 | + html += renderContextItem('Email Clicks (30d)', context.engagement.email_click_count_30d.toString()); |
| 2077 | + } |
| 2078 | + |
| 2079 | + if (context.slack_activity) { |
| 2080 | + html += renderContextItem('Slack Messages (30d)', context.slack_activity.total_messages_30d.toString()); |
| 2081 | + html += renderContextItem('Reactions Given', context.slack_activity.total_reactions_30d.toString()); |
| 2082 | + if (context.slack_activity.last_activity_at) { |
| 2083 | + html += renderContextItem('Last Slack Activity', new Date(context.slack_activity.last_activity_at).toLocaleDateString()); |
| 2084 | + } |
| 2085 | + } |
| 2086 | + |
| 2087 | + html += '</div></div>'; |
| 2088 | + |
| 2089 | + // Working Groups Section |
| 2090 | + if (context.working_groups && context.working_groups.length > 0) { |
| 2091 | + html += '<div style="margin-bottom: var(--space-4);">'; |
| 2092 | + html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Working Groups</h3>'; |
| 2093 | + html += '<div style="display: flex; flex-wrap: wrap; gap: var(--space-1);">'; |
| 2094 | + context.working_groups.forEach(wg => { |
| 2095 | + html += `<span style="display: inline-block; padding: 2px 8px; background: var(--color-primary-100); color: var(--color-primary-700); border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: var(--font-medium);">${escapeHtml(wg.name)}${wg.is_leader ? ' (Leader)' : ''}</span>`; |
| 2096 | + }); |
| 2097 | + html += '</div></div>'; |
| 2098 | + } |
| 2099 | + |
| 2100 | + // Insights Section |
| 2101 | + if (context.insights && context.insights.length > 0) { |
| 2102 | + html += '<div style="margin-bottom: var(--space-4);">'; |
| 2103 | + html += '<h3 style="font-size: var(--text-sm); margin-bottom: var(--space-2);">Insights</h3>'; |
| 2104 | + html += '<div style="max-height: 150px; overflow-y: auto;">'; |
| 2105 | + context.insights.forEach(insight => { |
| 2106 | + html += `<div style="background: var(--color-gray-50); border-radius: var(--radius-sm); padding: var(--space-2) var(--space-3); margin-bottom: var(--space-2);"> |
| 2107 | + <div style="font-weight: var(--font-medium); font-size: var(--text-xs); color: var(--color-text-heading);">${escapeHtml(insight.type_name || 'Insight')}</div> |
| 2108 | + <div style="font-size: var(--text-sm); color: var(--color-text-primary);">${escapeHtml(insight.value)}</div> |
| 2109 | + </div>`; |
| 2110 | + }); |
| 2111 | + html += '</div></div>'; |
| 2112 | + } |
| 2113 | + |
| 2114 | + // Link to full user view |
| 2115 | + html += `<div style="margin-top: var(--space-4); padding-top: var(--space-4); border-top: 1px solid var(--color-gray-200);"> |
| 2116 | + <a href="/admin/users" class="btn btn-secondary" style="text-decoration: none;">View in Users Page</a> |
| 2117 | + </div>`; |
| 2118 | + |
| 2119 | + return html; |
| 2120 | + } |
| 2121 | + |
| 2122 | + function renderContextItem(label, value) { |
| 2123 | + return ` |
| 2124 | + <div style="background: var(--color-gray-50); padding: var(--space-3); border-radius: var(--radius-md);"> |
| 2125 | + <div style="font-size: var(--text-xs); color: var(--color-text-muted); margin-bottom: var(--space-1);">${escapeHtml(label)}</div> |
| 2126 | + <div style="font-size: var(--text-sm); color: var(--color-text-heading); font-weight: var(--font-medium);">${escapeHtml(value)}</div> |
| 2127 | + </div> |
| 2128 | + `; |
| 2129 | + } |
| 2130 | + |
1950 | 2131 | // Close modal when clicking outside |
1951 | 2132 | window.onclick = function(event) { |
1952 | 2133 | if (event.target.classList.contains('modal')) { |
1953 | 2134 | event.target.style.display = 'none'; |
1954 | 2135 | } |
1955 | 2136 | } |
1956 | 2137 |
|
| 2138 | + // Close modals on Escape key |
| 2139 | + document.addEventListener('keydown', function(e) { |
| 2140 | + if (e.key === 'Escape') { |
| 2141 | + closeUserContextModal(); |
| 2142 | + } |
| 2143 | + }); |
| 2144 | + |
1957 | 2145 | // Check for link_domain query parameter |
1958 | 2146 | async function checkLinkDomainParam() { |
1959 | 2147 | const params = new URLSearchParams(window.location.search); |
|
0 commit comments