/**
* Command Sovereignty โ Auth & Dashboard App
* Wires Supabase Auth + data layer from Lauren Kingsley's engine
*/
// โโ Supabase Config โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const SB_URL = 'https://tbomwpzqpbujqhriqzra.supabase.co';
const SB_KEY = 'sb_publishable_DTwXPZUigdTML4YhCzoa3w_4awB0i7W';
// โโ DOM Helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => document.querySelectorAll(sel);
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let sb = null;
let profile = null;
let currentClient = null;
// โโ Init โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
document.addEventListener('DOMContentLoaded', () => {
initSupabase();
setupAuthUI();
checkSession();
});
// โโ Supabase Init โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function initSupabase() {
const { createClient } = window['supabase-js'];
sb = createClient(SB_URL, SB_KEY);
}
// โโ Session Check โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function checkSession() {
if (!sb) return;
const { data: { session } } = await sb.auth.getSession();
if (session) {
profile = await fetchProfile(session.user.id);
showApp();
} else {
showAuth();
}
}
// โโ Profile โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function fetchProfile(userId) {
const { data, error } = await sb
.from('profiles')
.select('*')
.eq('id', userId)
.single();
if (error && error.code !== 'PGRST116') throw error;
return data || { id: userId, username: 'user', role: 'member' };
}
// โโ Auth UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function setupAuthUI() {
const loginForm = $('#login-form');
const signupForm = $('#signup-form');
const logoutBtn = $('#logout-btn');
if (loginForm) {
loginForm.addEventListener('submit', handleLogin);
}
if (signupForm) {
signupForm.addEventListener('submit', handleSignup);
}
if (logoutBtn) {
logoutBtn.addEventListener('click', handleLogout);
}
// Toggle between login and signup
const toggleLinks = $$('.toggle-auth');
toggleLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const mode = link.dataset.mode;
if (mode === 'signup') {
$('#login-card').classList.add('hidden');
$('#signup-card').classList.remove('hidden');
} else {
$('#signup-card').classList.add('hidden');
$('#login-card').classList.remove('hidden');
}
});
});
}
async function handleLogin(e) {
e.preventDefault();
const email = $('#login-email').value;
const password = $('#login-password').value;
const status = $('#login-status');
status.textContent = 'Signing in...';
status.className = 'status-message loading';
const { data, error } = await sb.auth.signInWithPassword({ email, password });
if (error) {
status.textContent = error.message;
status.className = 'status-message error';
return;
}
status.textContent = 'Welcome back!';
status.className = 'status-message success';
profile = await fetchProfile(data.user.id);
setTimeout(() => showApp(), 500);
}
async function handleSignup(e) {
e.preventDefault();
const email = $('#signup-email').value;
const password = $('#signup-password').value;
const name = $('#signup-name').value;
const company = $('#signup-company').value;
const status = $('#signup-status');
status.textContent = 'Creating account...';
status.className = 'status-message loading';
const { data, error } = await sb.auth.signUp({
email,
password,
options: {
data: { username: name, company },
},
});
if (error) {
status.textContent = error.message;
status.className = 'status-message error';
return;
}
if (data.user && !data.user.email_confirmed_at) {
status.textContent = 'Check your email for a confirmation link!';
status.className = 'status-message success';
} else {
status.textContent = 'Account created!';
status.className = 'status-message success';
profile = await fetchProfile(data.user.id);
setTimeout(() => showApp(), 500);
}
}
async function handleLogout() {
await sb.auth.signOut();
profile = null;
currentClient = null;
showAuth();
}
// โโ View Switching โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function showAuth() {
$('#auth-view').classList.remove('hidden');
$('#app-view').classList.add('hidden');
}
function showApp() {
$('#auth-view').classList.add('hidden');
$('#app-view').classList.remove('hidden');
loadDashboard();
}
// โโ Dashboard โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function loadDashboard() {
if (!profile) return;
// Update header
const usernameEl = $('#username-display');
if (usernameEl) {
usernameEl.textContent = profile.username || profile.id.slice(0, 8);
}
// Load clients
const clientsEl = $('#clients-list');
if (clientsEl) {
clientsEl.innerHTML = '<div class="loading-spinner">Loading clients...</div>';
const clients = await fetchClients();
renderClients(clients, clientsEl);
}
// Load stats
const statsEl = $('#dashboard-stats');
if (statsEl) {
const stats = await fetchStats();
renderStats(stats, statsEl);
}
// Load recent activity
const activityEl = $('#activity-feed');
if (activityEl) {
const activity = await fetchActivity();
renderActivity(activity, activityEl);
}
}
// โโ Data Fetching โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function fetchClients() {
const { data, error } = await sb
.from('clients')
.select('*')
.order('name', { ascending: true });
if (error) {
console.error('Clients error:', error);
return [];
}
return data || [];
}
async function fetchClientData(clientId) {
const [projects, meetings, comments, briefings] = await Promise.all([
sb.from('projects').select('*').eq('client', clientId).order('progress', { ascending: false }),
sb.from('meetings').select('*').eq('client', clientId).order('met_on', { ascending: false }).limit(10),
sb.from('portal_comments').select('*').eq('client', clientId).order('created_at', { ascending: false }).limit(20),
sb.from('briefings').select('*').eq('client', clientId).order('week', { ascending: false }).limit(4),
]);
return {
projects: projects.data || [],
meetings: meetings.data || [],
comments: comments.data || [],
briefings: briefings.data || [],
};
}
async function fetchStats() {
const { data: clients } = await sb.from('clients').select('id');
const clientIds = clients?.map(c => c.id) || [];
let totalProjects = 0;
let totalMeetings = 0;
for (const clientId of clientIds) {
const { count: pCount } = await sb.from('projects').select('*', { count: 'exact', head: true }).eq('client', clientId);
const { count: mCount } = await sb.from('meetings').select('*', { count: 'exact', head: true }).eq('client', clientId);
totalProjects += pCount || 0;
totalMeetings += mCount || 0;
}
return {
clients: clients?.length || 0,
projects: totalProjects,
meetings: totalMeetings,
};
}
async function fetchActivity() {
const activity = [];
// Get recent meetings across all clients
const { data: meetings } = await sb
.from('meetings')
.select('*')
.order('met_on', { ascending: false })
.limit(10);
if (meetings) {
meetings.forEach(m => {
activity.push({
type: 'meeting',
title: m.title || 'Meeting',
client: m.client,
date: m.met_on,
});
});
}
return activity;
}
// โโ Rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function renderClients(clients, container) {
if (!clients || clients.length === 0) {
container.innerHTML = '<p class="empty-state">No clients yet. Add your first client to get started.</p>';
return;
}
container.innerHTML = clients.map(c => `
<div class="client-card" data-client-id="${c.id}">
<div class="client-name">${escapeHtml(c.name || 'Unnamed')}</div>
<div class="client-domain">${escapeHtml(c.domain || 'N/A')}</div>
<div class="client-meta">
${c.chat_enabled ? '<span class="badge active">Chat ON</span>' : '<span class="badge">Chat OFF</span>'}
</div>
<button class="btn-view" onclick="viewClient('${c.id}', '${escapeHtml(c.name)}')">View Portal โ</button>
</div>
`).join('');
}
function renderStats(stats, container) {
container.innerHTML = `
<div class="stat-card">
<div class="stat-value">${stats.clients}</div>
<div class="stat-label">Active Clients</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.projects}</div>
<div class="stat-label">Projects</div>
</div>
<div class="stat-card">
<div class="stat-value">${stats.meetings}</div>
<div class="stat-label">Meetings Held</div>
</div>
`;
}
function renderActivity(activity, container) {
if (!activity || activity.length === 0) {
container.innerHTML = '<p class="empty-state">No recent activity.</p>';
return;
}
container.innerHTML = activity.map(a => {
const date = new Date(a.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return `
<div class="activity-item">
<div class="activity-icon">๐</div>
<div class="activity-content">
<div class="activity-title">${escapeHtml(a.title)}</div>
<div class="activity-meta">${date} ยท Client Portal</div>
</div>
</div>
`;
}).join('');
}
// โโ Client Portal View โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
async function viewClient(clientId, clientName) {
currentClient = clientId;
// Update page title
$('#page-title').textContent = clientName || 'Client Portal';
$('#client-breadcrumb').textContent = clientName || 'Portal';
// Load client-specific data
const data = await fetchClientData(clientId);
renderClientPortal(data, clientName);
}
function renderClientPortal(data, clientName) {
// Projects
const projectsEl = $('#client-projects');
if (projectsEl) {
if (data.projects.length === 0) {
projectsEl.innerHTML = '<p class="empty-state">No projects for this client.</p>';
} else {
projectsEl.innerHTML = data.projects.map(p => `
<div class="project-item">
<div class="project-header">
<span class="project-name">${escapeHtml(p.name || 'Untitled')}</span>
${p.owner ? `<span class="project-owner">by ${escapeHtml(p.owner)}</span>` : ''}
</div>
<div class="project-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${p.progress || 0}%"></div>
</div>
<span class="progress-label">${p.progress || 0}%</span>
</div>
${p.next_step ? `<div class="project-next">Next: ${escapeHtml(p.next_step)}</div>` : ''}
</div>
`).join('');
}
}
// Meetings
const meetingsEl = $('#client-meetings');
if (meetingsEl) {
if (data.meetings.length === 0) {
meetingsEl.innerHTML = '<p class="empty-state">No meetings recorded.</p>';
} else {
meetingsEl.innerHTML = data.meetings.map(m => `
<div class="meeting-item">
<div class="meeting-title">${escapeHtml(m.title || 'Meeting')}</div>
<div class="meeting-date">${m.met_on ? new Date(m.met_on).toLocaleDateString() : 'N/A'}</div>
${m.notes ? `<div class="meeting-notes">${escapeHtml(m.notes.slice(0, 100))}${m.notes.length > 100 ? '...' : ''}</div>` : ''}
</div>
`).join('');
}
}
// Briefings
const briefingsEl = $('#client-briefings');
if (briefingsEl) {
if (data.briefings.length === 0) {
briefingsEl.innerHTML = '<p class="empty-state">No briefings this period.</p>';
} else {
briefingsEl.innerHTML = data.briefings.map(b => `
<div class="briefing-item">
<div class="briefing-week">Week ${b.week}</div>
</div>
`).join('');
}
}
// Switch to portal view
$('#dashboard-view').classList.add('hidden');
$('#portal-view').classList.remove('hidden');
}
function backToDashboard() {
$('#portal-view').classList.add('hidden');
$('#dashboard-view').classList.remove('hidden');
currentClient = null;
}
// โโ Utility โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function escapeHtml(text) {
if (!text) return '';
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}