Add admin, dashboards, learn, social, and video UI components

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-08 13:16:06 -03:00
parent 5f65a62808
commit cb33a75d39
24 changed files with 17830 additions and 1 deletions

View file

@ -0,0 +1,432 @@
<section class="accounts-settings">
<div class="accounts-header">
<h1>Connected Accounts</h1>
<p>Manage your social media and messaging channel integrations</p>
</div>
<div class="accounts-grid">
<div class="account-category">
<div class="category-header">
<h2>Social Media</h2>
<span class="category-count" id="socialCount">0 connected</span>
</div>
<div class="accounts-list" hx-get="/api/settings/accounts/social" hx-trigger="load" hx-swap="innerHTML">
<div class="account-card">
<div class="account-icon instagram">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
</svg>
</div>
<div class="account-info">
<span class="account-name">Instagram</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('instagram')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon facebook">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">Facebook</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('facebook')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon twitter">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M23 3a10.9 10.9 0 0 1-3.14 1.53 4.48 4.48 0 0 0-7.86 3v1A10.66 10.66 0 0 1 3 4s-4 9 5 13a11.64 11.64 0 0 1-7 2c9 5 20 0 20-11.5a4.5 4.5 0 0 0-.08-.83A7.72 7.72 0 0 0 23 3z"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">Twitter / X</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('twitter')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon linkedin">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path>
<rect x="2" y="9" width="4" height="12"></rect>
<circle cx="4" cy="4" r="2"></circle>
</svg>
</div>
<div class="account-info">
<span class="account-name">LinkedIn</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('linkedin')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon bluesky">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<circle cx="12" cy="12" r="10"></circle>
<path d="M8 12a4 4 0 0 1 8 0"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">Bluesky</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('bluesky')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon threads">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"></path>
<path d="M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6z"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">Threads</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('threads')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon tiktok">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M9 12a4 4 0 1 0 4 4V4a5 5 0 0 0 5 5"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">TikTok</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('tiktok')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon youtube">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M22.54 6.42a2.78 2.78 0 0 0-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 0 0-1.94 2A29 29 0 0 0 1 11.75a29 29 0 0 0 .46 5.33A2.78 2.78 0 0 0 3.4 19c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 0 0 1.94-2 29 29 0 0 0 .46-5.25 29 29 0 0 0-.46-5.33z"></path>
<polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02"></polygon>
</svg>
</div>
<div class="account-info">
<span class="account-name">YouTube</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('youtube')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon pinterest">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<circle cx="12" cy="12" r="10"></circle>
<path d="M8 12c0-2.2 1.8-4 4-4s4 1.8 4 4c0 2.5-2 4-4 6-2-2-4-3.5-4-6z"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">Pinterest</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('pinterest')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon reddit">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<circle cx="12" cy="12" r="10"></circle>
<circle cx="8" cy="12" r="1.5"></circle>
<circle cx="16" cy="12" r="1.5"></circle>
<path d="M8 16c1.5 1.5 5 1.5 8 0"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">Reddit</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('reddit')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon snapchat">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z"></path>
<path d="M12 8c-2 0-3 1.5-3 3.5 0 1 .5 2 1.5 2.5-1 .5-2 1-2 1.5 0 1 2 2 3.5 2s3.5-1 3.5-2c0-.5-1-1-2-1.5 1-.5 1.5-1.5 1.5-2.5 0-2-1-3.5-3-3.5z"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">Snapchat</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('snapchat')">Connect</button>
</div>
</div>
</div>
<div class="account-category">
<div class="category-header">
<h2>Messaging</h2>
<span class="category-count" id="messagingCount">0 connected</span>
</div>
<div class="accounts-list" hx-get="/api/settings/accounts/messaging" hx-trigger="load" hx-swap="innerHTML">
<div class="account-card">
<div class="account-icon discord">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03z"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">Discord</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('discord')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon whatsapp">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">WhatsApp Business</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('whatsapp')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon telegram">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<line x1="22" y1="2" x2="11" y2="13"></line>
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
</svg>
</div>
<div class="account-info">
<span class="account-name">Telegram</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('telegram')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon twilio">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">Twilio SMS</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('twilio')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon wechat">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M8.5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path>
<path d="M13.5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path>
<path d="M9 16a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path>
<path d="M15 16a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"></path>
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12c0 1.6.376 3.112 1.043 4.453L2 22l5.547-1.043A9.96 9.96 0 0 0 12 22z"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">WeChat</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('wechat')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon teams">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
</div>
<div class="account-info">
<span class="account-name">Microsoft Teams</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('teams')">Connect</button>
</div>
</div>
</div>
<div class="account-category">
<div class="category-header">
<h2>Email</h2>
<span class="category-count" id="emailCount">0 connected</span>
</div>
<div class="accounts-list" hx-get="/api/settings/accounts/email" hx-trigger="load" hx-swap="innerHTML">
<div class="account-card">
<div class="account-icon gmail">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
</div>
<div class="account-info">
<span class="account-name">Gmail / Google Workspace</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('gmail')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon outlook">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"></path>
<polyline points="22,6 12,13 2,6"></polyline>
</svg>
</div>
<div class="account-info">
<span class="account-name">Outlook / Office 365</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="connectAccount('outlook')">Connect</button>
</div>
<div class="account-card">
<div class="account-icon smtp">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
</div>
<div class="account-info">
<span class="account-name">Custom SMTP</span>
<span class="account-status disconnected">Not connected</span>
</div>
<button class="btn-connect" onclick="showSmtpModal()">Configure</button>
</div>
</div>
</div>
</div>
</section>
<dialog id="accountModal" class="account-modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modalTitle">Connect Account</h2>
<button class="btn-close" onclick="closeModal('accountModal')">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body" id="modalBody">
<div class="oauth-loading">
<div class="spinner"></div>
<p>Redirecting to authorization...</p>
</div>
</div>
</div>
</dialog>
<dialog id="smtpModal" class="account-modal">
<div class="modal-content">
<div class="modal-header">
<h2>Configure SMTP Server</h2>
<button class="btn-close" onclick="closeModal('smtpModal')">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<form id="smtpForm" hx-post="/api/settings/accounts/smtp" hx-swap="none">
<div class="form-group">
<label for="smtpHost">SMTP Host</label>
<input type="text" id="smtpHost" name="host" required placeholder="smtp.example.com">
</div>
<div class="form-row">
<div class="form-group">
<label for="smtpPort">Port</label>
<input type="number" id="smtpPort" name="port" required value="587">
</div>
<div class="form-group">
<label for="smtpSecurity">Security</label>
<select id="smtpSecurity" name="security">
<option value="tls">TLS</option>
<option value="ssl">SSL</option>
<option value="none">None</option>
</select>
</div>
</div>
<div class="form-group">
<label for="smtpUsername">Username</label>
<input type="text" id="smtpUsername" name="username" required placeholder="your@email.com">
</div>
<div class="form-group">
<label for="smtpPassword">Password</label>
<input type="password" id="smtpPassword" name="password" required placeholder="••••••••">
</div>
<div class="form-group">
<label for="smtpFrom">From Address</label>
<input type="email" id="smtpFrom" name="from_address" required placeholder="noreply@example.com">
</div>
<div class="form-group">
<label for="smtpFromName">From Name</label>
<input type="text" id="smtpFromName" name="from_name" placeholder="Your Company">
</div>
<div class="form-actions">
<button type="button" class="btn-secondary" onclick="testSmtpConnection()">Test Connection</button>
<button type="submit" class="btn-primary">Save Configuration</button>
</div>
</form>
</div>
</div>
</dialog>
<dialog id="accountDetailsModal" class="account-modal wide">
<div class="modal-content">
<div class="modal-header">
<h2 id="detailsTitle">Account Details</h2>
<button class="btn-close" onclick="closeModal('accountDetailsModal')">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body" id="detailsBody">
<div class="account-details-grid">
<div class="detail-section">
<h3>Connection Status</h3>
<div class="status-indicator connected">
<span class="status-dot"></span>
<span>Connected</span>
</div>
<p class="connected-as">Connected as: <strong id="connectedAccount">@username</strong></p>
<p class="connected-since">Since: <span id="connectedSince">January 15, 2025</span></p>
</div>
<div class="detail-section">
<h3>Permissions</h3>
<ul class="permissions-list" id="permissionsList">
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Read posts
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Create posts
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">

View file

@ -0,0 +1,558 @@
<section class="admin-dashboard">
<div class="dashboard-header">
<div class="header-title">
<h1>Organization Dashboard</h1>
<p>Overview of your organization's activity, members, and system health</p>
</div>
<div class="header-actions">
<button class="btn-secondary" hx-get="/api/admin/export-report" hx-swap="none">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Export Report
</button>
<button class="btn-primary" onclick="showInviteMemberModal()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
<line x1="20" y1="8" x2="20" y2="14"></line>
<line x1="23" y1="11" x2="17" y2="11"></line>
</svg>
Invite Member
</button>
</div>
</div>
<div class="stats-overview" hx-get="/api/admin/dashboard/stats" hx-trigger="load" hx-swap="innerHTML">
<div class="stat-card members">
<div class="stat-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
</div>
<div class="stat-content">
<span class="stat-value" id="totalMembers">24</span>
<span class="stat-label">Team Members</span>
<span class="stat-change positive">+3 this month</span>
</div>
</div>
<div class="stat-card bots">
<div class="stat-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<rect x="3" y="11" width="18" height="10" rx="2"></rect>
<circle cx="12" cy="5" r="2"></circle>
<path d="M12 7v4"></path>
<line x1="8" y1="16" x2="8" y2="16"></line>
<line x1="16" y1="16" x2="16" y2="16"></line>
</svg>
</div>
<div class="stat-content">
<span class="stat-value" id="activeBots">8</span>
<span class="stat-label">Active Bots</span>
<span class="stat-change neutral">No change</span>
</div>
</div>
<div class="stat-card conversations">
<div class="stat-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
</svg>
</div>
<div class="stat-content">
<span class="stat-value" id="totalConversations">12.4K</span>
<span class="stat-label">Conversations</span>
<span class="stat-change positive">+18% this week</span>
</div>
</div>
<div class="stat-card uptime">
<div class="stat-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
</svg>
</div>
<div class="stat-content">
<span class="stat-value" id="systemUptime">99.9%</span>
<span class="stat-label">System Uptime</span>
<span class="stat-change positive">Last 30 days</span>
</div>
</div>
</div>
<div class="dashboard-grid">
<div class="card system-health">
<div class="card-header">
<h2>System Health</h2>
<span class="health-badge healthy" id="overallHealth">All Systems Operational</span>
</div>
<div class="health-items" hx-get="/api/admin/dashboard/health" hx-trigger="load, every 30s" hx-swap="innerHTML">
<div class="health-item">
<div class="health-indicator healthy"></div>
<div class="health-info">
<span class="health-name">API Server</span>
<span class="health-status">Operational</span>
</div>
<div class="health-metrics">
<span class="metric">Latency: 45ms</span>
<span class="metric">Requests: 1.2K/min</span>
</div>
</div>
<div class="health-item">
<div class="health-indicator healthy"></div>
<div class="health-info">
<span class="health-name">Database</span>
<span class="health-status">Operational</span>
</div>
<div class="health-metrics">
<span class="metric">Connections: 45/100</span>
<span class="metric">Query Time: 12ms</span>
</div>
</div>
<div class="health-item">
<div class="health-indicator healthy"></div>
<div class="health-info">
<span class="health-name">Vector Database</span>
<span class="health-status">Operational</span>
</div>
<div class="health-metrics">
<span class="metric">Vectors: 2.4M</span>
<span class="metric">Search: 8ms</span>
</div>
</div>
<div class="health-item">
<div class="health-indicator healthy"></div>
<div class="health-info">
<span class="health-name">LLM Service</span>
<span class="health-status">Operational</span>
</div>
<div class="health-metrics">
<span class="metric">Tokens: 125K/min</span>
<span class="metric">Latency: 850ms</span>
</div>
</div>
<div class="health-item">
<div class="health-indicator warning"></div>
<div class="health-info">
<span class="health-name">Cache</span>
<span class="health-status">High Usage</span>
</div>
<div class="health-metrics">
<span class="metric">Memory: 85%</span>
<span class="metric">Hit Rate: 94%</span>
</div>
</div>
<div class="health-item">
<div class="health-indicator healthy"></div>
<div class="health-info">
<span class="health-name">Storage</span>
<span class="health-status">Operational</span>
</div>
<div class="health-metrics">
<span class="metric">Used: 34.5GB</span>
<span class="metric">IOPS: 2.1K</span>
</div>
</div>
</div>
</div>
<div class="card activity-feed">
<div class="card-header">
<h2>Recent Activity</h2>
<div class="activity-filters">
<button class="filter-btn active" data-filter="all">All</button>
<button class="filter-btn" data-filter="members">Members</button>
<button class="filter-btn" data-filter="bots">Bots</button>
<button class="filter-btn" data-filter="security">Security</button>
</div>
</div>
<div class="activity-list" hx-get="/api/admin/dashboard/activity" hx-trigger="load" hx-swap="innerHTML">
<div class="activity-item">
<div class="activity-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar">
</div>
<div class="activity-content">
<div class="activity-text">
<strong>Sarah Johnson</strong> joined the organization as <span class="role-badge">Developer</span>
</div>
<span class="activity-time">5 minutes ago</span>
</div>
</div>
<div class="activity-item">
<div class="activity-avatar bot">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<rect x="3" y="11" width="18" height="10" rx="2"></rect>
<circle cx="12" cy="5" r="2"></circle>
<path d="M12 7v4"></path>
</svg>
</div>
<div class="activity-content">
<div class="activity-text">
<strong>Customer Support Bot</strong> was updated with new knowledge base
</div>
<span class="activity-time">23 minutes ago</span>
</div>
</div>
<div class="activity-item">
<div class="activity-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar">
</div>
<div class="activity-content">
<div class="activity-text">
<strong>Mike Chen</strong> changed role from <span class="role-badge">Member</span> to <span class="role-badge">Manager</span>
</div>
<span class="activity-time">1 hour ago</span>
</div>
</div>
<div class="activity-item security">
<div class="activity-avatar security">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
</div>
<div class="activity-content">
<div class="activity-text">
<strong>Security Alert:</strong> New login from unrecognized device for <strong>admin@company.com</strong>
</div>
<span class="activity-time">2 hours ago</span>
</div>
</div>
<div class="activity-item">
<div class="activity-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar">
</div>
<div class="activity-content">
<div class="activity-text">
<strong>Emily Davis</strong> created a new form <span class="resource-link">Customer Feedback Survey</span>
</div>
<span class="activity-time">3 hours ago</span>
</div>
</div>
<div class="activity-item">
<div class="activity-avatar bot">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<rect x="3" y="11" width="18" height="10" rx="2"></rect>
<circle cx="12" cy="5" r="2"></circle>
<path d="M12 7v4"></path>
</svg>
</div>
<div class="activity-content">
<div class="activity-text">
<strong>Sales Assistant</strong> reached 10,000 conversations milestone
</div>
<span class="activity-time">5 hours ago</span>
</div>
</div>
</div>
<button class="btn-text load-more" hx-get="/api/admin/dashboard/activity?page=2" hx-target=".activity-list" hx-swap="beforeend">
Load More Activity
</button>
</div>
<div class="card member-overview">
<div class="card-header">
<h2>Team Members</h2>
<a href="/admin/users" class="view-all-link">View All</a>
</div>
<div class="member-stats">
<div class="member-stat">
<span class="stat-number">18</span>
<span class="stat-desc">Active</span>
</div>
<div class="member-stat">
<span class="stat-number">4</span>
<span class="stat-desc">Pending</span>
</div>
<div class="member-stat">
<span class="stat-number">2</span>
<span class="stat-desc">Inactive</span>
</div>
</div>
<div class="member-list" hx-get="/api/admin/dashboard/members" hx-trigger="load" hx-swap="innerHTML">
<div class="member-item">
<div class="member-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar">
<span class="status-indicator online"></span>
</div>
<div class="member-info">
<span class="member-name">John Smith</span>
<span class="member-role">Owner</span>
</div>
<span class="member-status online">Online</span>
</div>
<div class="member-item">
<div class="member-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar">
<span class="status-indicator online"></span>
</div>
<div class="member-info">
<span class="member-name">Sarah Johnson</span>
<span class="member-role">Admin</span>
</div>
<span class="member-status online">Online</span>
</div>
<div class="member-item">
<div class="member-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar">
<span class="status-indicator away"></span>
</div>
<div class="member-info">
<span class="member-name">Mike Chen</span>
<span class="member-role">Manager</span>
</div>
<span class="member-status away">Away</span>
</div>
<div class="member-item">
<div class="member-avatar">
<img src="/assets/avatars/default.svg" alt="User avatar">
<span class="status-indicator offline"></span>
</div>
<div class="member-info">
<span class="member-name">Emily Davis</span>
<span class="member-role">Member</span>
</div>
<span class="member-status offline">Offline</span>
</div>
</div>
</div>
<div class="card role-distribution">
<div class="card-header">
<h2>Role Distribution</h2>
<a href="/admin/roles" class="view-all-link">Manage Roles</a>
</div>
<div class="role-chart" hx-get="/api/admin/dashboard/roles" hx-trigger="load" hx-swap="innerHTML">
<div class="role-bars">
<div class="role-bar-item">
<div class="role-bar-label">
<span class="role-name">Owner</span>
<span class="role-count">1</span>
</div>
<div class="role-bar">
<div class="role-bar-fill" style="width: 4%; background: var(--chart-1)"></div>
</div>
</div>
<div class="role-bar-item">
<div class="role-bar-label">
<span class="role-name">Admin</span>
<span class="role-count">3</span>
</div>
<div class="role-bar">
<div class="role-bar-fill" style="width: 12%; background: var(--chart-2)"></div>
</div>
</div>
<div class="role-bar-item">
<div class="role-bar-label">
<span class="role-name">Manager</span>
<span class="role-count">5</span>
</div>
<div class="role-bar">
<div class="role-bar-fill" style="width: 21%; background: var(--chart-3)"></div>
</div>
</div>
<div class="role-bar-item">
<div class="role-bar-label">
<span class="role-name">Member</span>
<span class="role-count">12</span>
</div>
<div class="role-bar">
<div class="role-bar-fill" style="width: 50%; background: var(--chart-4)"></div>
</div>
</div>
<div class="role-bar-item">
<div class="role-bar-label">
<span class="role-name">Viewer</span>
<span class="role-count">3</span>
</div>
<div class="role-bar">
<div class="role-bar-fill" style="width: 12%; background: var(--chart-5)"></div>
</div>
</div>
</div>
</div>
</div>
<div class="card bot-status">
<div class="card-header">
<h2>Bot Status</h2>
<a href="/bots" class="view-all-link">Manage Bots</a>
</div>
<div class="bot-list" hx-get="/api/admin/dashboard/bots" hx-trigger="load" hx-swap="innerHTML">
<div class="bot-item">
<div class="bot-avatar">CS</div>
<div class="bot-info">
<span class="bot-name">Customer Support Bot</span>
<span class="bot-desc">Handles customer inquiries</span>
</div>
<div class="bot-metrics">
<span class="bot-metric">1.2K chats today</span>
</div>
<span class="bot-status running">Running</span>
</div>
<div class="bot-item">
<div class="bot-avatar">SA</div>
<div class="bot-info">
<span class="bot-name">Sales Assistant</span>
<span class="bot-desc">Product recommendations</span>
</div>
<div class="bot-metrics">
<span class="bot-metric">856 chats today</span>
</div>
<span class="bot-status running">Running</span>
</div>
<div class="bot-item">
<div class="bot-avatar">HR</div>
<div class="bot-info">
<span class="bot-name">HR Assistant</span>
<span class="bot-desc">Employee questions</span>
</div>
<div class="bot-metrics">
<span class="bot-metric">234 chats today</span>
</div>
<span class="bot-status running">Running</span>
</div>
<div class="bot-item">
<div class="bot-avatar">IT</div>
<div class="bot-info">
<span class="bot-name">IT Helpdesk</span>
<span class="bot-desc">Technical support</span>
</div>
<div class="bot-metrics">
<span class="bot-metric">145 chats today</span>
</div>
<span class="bot-status paused">Paused</span>
</div>
</div>
</div>
<div class="card pending-invitations">
<div class="card-header">
<h2>Pending Invitations</h2>
<button class="btn-secondary btn-sm" onclick="showBulkInviteModal()">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="8.5" cy="7" r="4"></circle>
<line x1="20" y1="8" x2="20" y2="14"></line>
<line x1="23" y1="11" x2="17" y2="11"></line>
</svg>
Bulk Invite
</button>
</div>
<div class="invitation-list" hx-get="/api/admin/dashboard/invitations" hx-trigger="load" hx-swap="innerHTML">
<div class="invitation-item">
<div class="invitation-info">
<span class="invitation-email">jane.doe@example.com</span>
<span class="invitation-role">Member</span>
</div>
<div class="invitation-meta">
<span class="invitation-sent">Sent 2 days ago</span>
<span class="invitation-expires">Expires in 5 days</span>
</div>
<div class="invitation-actions">
<button class="btn-icon" title="Resend invitation" hx-post="/api/admin/invitations/resend/inv_001" hx-swap="none">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
<button class="btn-icon danger" title="Revoke invitation" hx-delete="/api/admin/invitations/inv_001" hx-swap="none" hx-confirm="Revoke this invitation?">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<div class="invitation-item">
<div class="invitation-info">
<span class="invitation-email">bob.wilson@example.com</span>
<span class="invitation-role">Developer</span>
</div>
<div class="invitation-meta">
<span class="invitation-sent">Sent 5 days ago</span>
<span class="invitation-expires warning">Expires in 2 days</span>
</div>
<div class="invitation-actions">
<button class="btn-icon" title="Resend invitation" hx-post="/api/admin/invitations/resend/inv_002" hx-swap="none">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
<button class="btn-icon danger" title="Revoke invitation" hx-delete="/api/admin/invitations/inv_002" hx-swap="none" hx-confirm="Revoke this invitation?">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<div class="invitation-item">
<div class="invitation-info">
<span class="invitation-email">alice.smith@example.com</span>
<span class="invitation-role">Manager</span>
</div>
<div class="invitation-meta">
<span class="invitation-sent">Sent 1 day ago</span>
<span class="invitation-expires">Expires in 6 days</span>
</div>
<div class="invitation-actions">
<button class="btn-icon" title="Resend invitation" hx-post="/api/admin/invitations/resend/inv_003" hx-swap="none">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
<button class="btn-icon danger" title="Revoke invitation" hx-delete="/api/admin/invitations/inv_003" hx-swap="none" hx-confirm="Revoke this invitation?">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
</div>
</div>
<div class="card quick-actions">
<div class="card-header">
<h2>Quick Actions</h2>
</div>
<div class="actions-grid">
<a href="/admin/users" class="action-item">
<div class="action-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
</div>
<span class="action-label">Manage Users</span>
</a>
<a href="/admin/roles" class="action-item">
<div class="action-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
</div>
<span class="action-label">Roles & Permissions</span>
</a>
<a href="/admin/groups" class="action-item">
<div class="action-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
</div>
<span class="action-label">Groups</span>
</a>
<a href="/admin/billing" class="action-item">
<div class="action-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>

View file

@ -0,0 +1,467 @@
<section class="billing-dashboard-analytics">
<div class="dashboard-header">
<div class="header-title">
<h1>Billing Dashboard</h1>
<p>Comprehensive analytics and insights for your organization's billing and usage</p>
</div>
<div class="header-actions">
<select class="period-selector" id="billingPeriod" onchange="updateBillingPeriod(this.value)">
<option value="current">Current Period</option>
<option value="last30">Last 30 Days</option>
<option value="last90">Last 90 Days</option>
<option value="ytd">Year to Date</option>
<option value="custom">Custom Range</option>
</select>
<button class="btn-secondary" onclick="exportBillingReport()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Export Report
</button>
</div>
</div>
<div class="metrics-overview" hx-get="/api/billing/dashboard/metrics" hx-trigger="load" hx-swap="innerHTML">
<div class="metric-card spending">
<div class="metric-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<line x1="12" y1="1" x2="12" y2="23"></line>
<path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"></path>
</svg>
</div>
<div class="metric-content">
<span class="metric-label">Current Spending</span>
<span class="metric-value" id="currentSpending">$247.50</span>
<span class="metric-change positive">
<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
</svg>
12% from last period
</span>
</div>
</div>
<div class="metric-card quota">
<div class="metric-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
</svg>
</div>
<div class="metric-content">
<span class="metric-label">Quota Utilization</span>
<span class="metric-value" id="quotaUtilization">67%</span>
<span class="metric-change neutral">Average across all quotas</span>
</div>
</div>
<div class="metric-card api-calls">
<div class="metric-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
</svg>
</div>
<div class="metric-content">
<span class="metric-label">API Calls</span>
<span class="metric-value" id="apiCallsCount">1.2M</span>
<span class="metric-change positive">+23% from last period</span>
</div>
</div>
<div class="metric-card storage">
<div class="metric-icon">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
</svg>
</div>
<div class="metric-content">
<span class="metric-label">Storage Used</span>
<span class="metric-value" id="storageUsed">34.5 GB</span>
<span class="metric-change warning">69% of 50 GB limit</span>
</div>
</div>
</div>
<div class="dashboard-grid">
<div class="chart-card spending-trends">
<div class="card-header">
<h2>Spending Trends</h2>
<div class="chart-controls">
<button class="chart-btn active" data-view="daily">Daily</button>
<button class="chart-btn" data-view="weekly">Weekly</button>
<button class="chart-btn" data-view="monthly">Monthly</button>
</div>
</div>
<div class="chart-container" id="spendingChart" hx-get="/api/billing/dashboard/spending-chart" hx-trigger="load" hx-swap="innerHTML">
<div class="chart-placeholder">
<svg viewBox="0 0 400 200" class="spending-svg">
<defs>
<linearGradient id="spendingGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:var(--primary);stop-opacity:0.3" />
<stop offset="100%" style="stop-color:var(--primary);stop-opacity:0" />
</linearGradient>
</defs>
<path class="chart-area" fill="url(#spendingGradient)" d="M0,150 L50,120 L100,130 L150,90 L200,100 L250,70 L300,80 L350,50 L400,60 L400,200 L0,200 Z"></path>
<path class="chart-line" fill="none" stroke="var(--primary)" stroke-width="2" d="M0,150 L50,120 L100,130 L150,90 L200,100 L250,70 L300,80 L350,50 L400,60"></path>
<g class="chart-points">
<circle cx="0" cy="150" r="4" fill="var(--primary)"></circle>
<circle cx="50" cy="120" r="4" fill="var(--primary)"></circle>
<circle cx="100" cy="130" r="4" fill="var(--primary)"></circle>
<circle cx="150" cy="90" r="4" fill="var(--primary)"></circle>
<circle cx="200" cy="100" r="4" fill="var(--primary)"></circle>
<circle cx="250" cy="70" r="4" fill="var(--primary)"></circle>
<circle cx="300" cy="80" r="4" fill="var(--primary)"></circle>
<circle cx="350" cy="50" r="4" fill="var(--primary)"></circle>
<circle cx="400" cy="60" r="4" fill="var(--primary)"></circle>
</g>
</svg>
<div class="chart-labels">
<span>Jan 14</span>
<span>Jan 15</span>
<span>Jan 16</span>
<span>Jan 17</span>
<span>Jan 18</span>
<span>Jan 19</span>
<span>Jan 20</span>
<span>Jan 21</span>
<span>Jan 22</span>
</div>
</div>
</div>
</div>
<div class="chart-card usage-breakdown">
<div class="card-header">
<h2>Cost Breakdown by Service</h2>
<button class="btn-icon" onclick="toggleBreakdownView()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
<line x1="11" y1="8" x2="11" y2="14"></line>
<line x1="8" y1="11" x2="14" y2="11"></line>
</svg>
</button>
</div>
<div class="breakdown-chart" hx-get="/api/billing/dashboard/cost-breakdown" hx-trigger="load" hx-swap="innerHTML">
<div class="donut-chart">
<svg viewBox="0 0 100 100" class="donut-svg">
<circle cx="50" cy="50" r="40" fill="none" stroke="var(--surface-border)" stroke-width="8"></circle>
<circle cx="50" cy="50" r="40" fill="none" stroke="var(--chart-1)" stroke-width="8" stroke-dasharray="100.53 150.79" stroke-dashoffset="0" transform="rotate(-90 50 50)"></circle>
<circle cx="50" cy="50" r="40" fill="none" stroke="var(--chart-2)" stroke-width="8" stroke-dasharray="62.83 188.49" stroke-dashoffset="-100.53" transform="rotate(-90 50 50)"></circle>
<circle cx="50" cy="50" r="40" fill="none" stroke="var(--chart-3)" stroke-width="8" stroke-dasharray="50.26 200.06" stroke-dashoffset="-163.36" transform="rotate(-90 50 50)"></circle>
<circle cx="50" cy="50" r="40" fill="none" stroke="var(--chart-4)" stroke-width="8" stroke-dasharray="37.70 213.62" stroke-dashoffset="-213.62" transform="rotate(-90 50 50)"></circle>
</svg>
<div class="donut-center">
<span class="donut-value">$247.50</span>
<span class="donut-label">Total</span>
</div>
</div>
<div class="breakdown-legend">
<div class="legend-item">
<span class="legend-color" style="background: var(--chart-1)"></span>
<span class="legend-label">API Calls</span>
<span class="legend-value">$99.00 (40%)</span>
</div>
<div class="legend-item">
<span class="legend-color" style="background: var(--chart-2)"></span>
<span class="legend-label">Storage</span>
<span class="legend-value">$61.88 (25%)</span>
</div>
<div class="legend-item">
<span class="legend-color" style="background: var(--chart-3)"></span>
<span class="legend-label">Messages</span>
<span class="legend-value">$49.50 (20%)</span>
</div>
<div class="legend-item">
<span class="legend-color" style="background: var(--chart-4)"></span>
<span class="legend-label">Other</span>
<span class="legend-value">$37.12 (15%)</span>
</div>
</div>
</div>
</div>
<div class="card quota-management">
<div class="card-header">
<h2>Quota Management</h2>
<button class="btn-secondary btn-sm" onclick="showQuotaSettings()">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
Settings
</button>
</div>
<div class="quota-list" hx-get="/api/billing/dashboard/quotas" hx-trigger="load" hx-swap="innerHTML">
<div class="quota-item">
<div class="quota-header">
<div class="quota-info">
<span class="quota-name">API Calls</span>
<span class="quota-usage">1,247,500 / 2,000,000</span>
</div>
<span class="quota-percent warning">62%</span>
</div>
<div class="quota-bar">
<div class="quota-fill warning" style="width: 62%"></div>
<div class="quota-threshold" style="left: 80%"></div>
<div class="quota-threshold critical" style="left: 90%"></div>
</div>
<div class="quota-alerts">
<span class="alert-indicator active">80% alert enabled</span>
</div>
</div>
<div class="quota-item">
<div class="quota-header">
<div class="quota-info">
<span class="quota-name">Storage</span>
<span class="quota-usage">34.5 GB / 50 GB</span>
</div>
<span class="quota-percent warning">69%</span>
</div>
<div class="quota-bar">
<div class="quota-fill warning" style="width: 69%"></div>
<div class="quota-threshold" style="left: 80%"></div>
<div class="quota-threshold critical" style="left: 90%"></div>
</div>
<div class="quota-alerts">
<span class="alert-indicator active">80% alert enabled</span>
</div>
</div>
<div class="quota-item">
<div class="quota-header">
<div class="quota-info">
<span class="quota-name">Messages</span>
<span class="quota-usage">45,000 / 100,000</span>
</div>
<span class="quota-percent normal">45%</span>
</div>
<div class="quota-bar">
<div class="quota-fill" style="width: 45%"></div>
<div class="quota-threshold" style="left: 80%"></div>
<div class="quota-threshold critical" style="left: 90%"></div>
</div>
<div class="quota-alerts">
<span class="alert-indicator">80% alert enabled</span>
</div>
</div>
<div class="quota-item">
<div class="quota-header">
<div class="quota-info">
<span class="quota-name">Team Members</span>
<span class="quota-usage">12 / 50</span>
</div>
<span class="quota-percent normal">24%</span>
</div>
<div class="quota-bar">
<div class="quota-fill" style="width: 24%"></div>
<div class="quota-threshold" style="left: 80%"></div>
<div class="quota-threshold critical" style="left: 90%"></div>
</div>
<div class="quota-alerts">
<span class="alert-indicator">No alerts configured</span>
</div>
</div>
<div class="quota-item">
<div class="quota-header">
<div class="quota-info">
<span class="quota-name">Bots</span>
<span class="quota-usage">8 / 20</span>
</div>
<span class="quota-percent normal">40%</span>
</div>
<div class="quota-bar">
<div class="quota-fill" style="width: 40%"></div>
<div class="quota-threshold" style="left: 80%"></div>
<div class="quota-threshold critical" style="left: 90%"></div>
</div>
<div class="quota-alerts">
<span class="alert-indicator">No alerts configured</span>
</div>
</div>
</div>
</div>
<div class="card usage-by-bot">
<div class="card-header">
<h2>Usage by Bot</h2>
<select class="metric-selector" id="botMetric" onchange="updateBotMetric(this.value)">
<option value="api_calls">API Calls</option>
<option value="messages">Messages</option>
<option value="storage">Storage</option>
<option value="cost">Cost</option>
</select>
</div>
<div class="bot-usage-list" hx-get="/api/billing/dashboard/bot-usage" hx-trigger="load" hx-swap="innerHTML">
<div class="bot-usage-item">
<div class="bot-info">
<div class="bot-avatar">CS</div>
<div class="bot-details">
<span class="bot-name">Customer Support Bot</span>
<span class="bot-id">bot_cs_01</span>
</div>
</div>
<div class="bot-metrics">
<div class="bot-metric-bar">
<div class="bot-metric-fill" style="width: 45%"></div>
</div>
<span class="bot-metric-value">562,500 calls</span>
</div>
</div>
<div class="bot-usage-item">
<div class="bot-info">
<div class="bot-avatar">SA</div>
<div class="bot-details">
<span class="bot-name">Sales Assistant</span>
<span class="bot-id">bot_sa_01</span>
</div>
</div>
<div class="bot-metrics">
<div class="bot-metric-bar">
<div class="bot-metric-fill" style="width: 32%"></div>
</div>
<span class="bot-metric-value">400,000 calls</span>
</div>
</div>
<div class="bot-usage-item">
<div class="bot-info">
<div class="bot-avatar">HR</div>
<div class="bot-details">
<span class="bot-name">HR Assistant</span>
<span class="bot-id">bot_hr_01</span>
</div>
</div>
<div class="bot-metrics">
<div class="bot-metric-bar">
<div class="bot-metric-fill" style="width: 18%"></div>
</div>
<span class="bot-metric-value">225,000 calls</span>
</div>
</div>
<div class="bot-usage-item">
<div class="bot-info">
<div class="bot-avatar">IT</div>
<div class="bot-details">
<span class="bot-name">IT Helpdesk</span>
<span class="bot-id">bot_it_01</span>
</div>
</div>
<div class="bot-metrics">
<div class="bot-metric-bar">
<div class="bot-metric-fill" style="width: 5%"></div>
</div>
<span class="bot-metric-value">60,000 calls</span>
</div>
</div>
</div>
</div>
<div class="card recent-invoices">
<div class="card-header">
<h2>Recent Invoices</h2>
<a href="/admin/billing" class="view-all-link">View All</a>
</div>
<div class="invoices-list" hx-get="/api/billing/dashboard/recent-invoices" hx-trigger="load" hx-swap="innerHTML">
<div class="invoice-item">
<div class="invoice-info">
<span class="invoice-id">INV-2025-0122</span>
<span class="invoice-date">Jan 21, 2025</span>
</div>
<div class="invoice-amount">$247.50</div>
<span class="invoice-status paid">Paid</span>
<button class="btn-icon" onclick="downloadInvoice('INV-2025-0122')">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
</div>
<div class="invoice-item">
<div class="invoice-info">
<span class="invoice-id">INV-2024-1221</span>
<span class="invoice-date">Dec 21, 2024</span>
</div>
<div class="invoice-amount">$198.00</div>
<span class="invoice-status paid">Paid</span>
<button class="btn-icon" onclick="downloadInvoice('INV-2024-1221')">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
</div>
<div class="invoice-item">
<div class="invoice-info">
<span class="invoice-id">INV-2024-1121</span>
<span class="invoice-date">Nov 21, 2024</span>
</div>
<div class="invoice-amount">$185.25</div>
<span class="invoice-status paid">Paid</span>
<button class="btn-icon" onclick="downloadInvoice('INV-2024-1121')">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
</div>
</div>
</div>
<div class="card alerts-activity">
<div class="card-header">
<h2>Billing Alerts</h2>
<button class="btn-secondary btn-sm" onclick="configureAlerts()">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
Configure
</button>
</div>
<div class="alerts-list" hx-get="/api/billing/dashboard/alerts" hx-trigger="load" hx-swap="innerHTML">
<div class="alert-item warning">
<div class="alert-icon">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</div>
<div class="alert-content">
<span class="alert-title">Storage approaching 70%</span>
<span class="alert-time">2 hours ago</span>
</div>
<button class="btn-icon dismiss" onclick="dismissAlert(this)">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="alert-item info">
<div class="alert-icon">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
</div>
<div class="alert-content">
<span class="alert-title">Invoice generated for January</span>
<span class="alert-time">1 day ago</span>
</div>
<button class="btn-icon dismiss" onclick="dismissAlert(this)">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="alert-item success">
<div class="alert-icon">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>

777
ui/suite/admin/billing.html Normal file
View file

@ -0,0 +1,777 @@
<section class="billing-dashboard">
<div class="billing-header">
<h1>Billing & Subscription</h1>
<p>Manage your organization's subscription, payment methods, and billing history</p>
</div>
<div class="billing-grid">
<div class="billing-card current-plan">
<div class="card-header">
<h2>Current Plan</h2>
<span class="plan-badge pro">Pro</span>
</div>
<div class="plan-details">
<div class="plan-price">
<span class="amount">$49</span>
<span class="period">/month</span>
</div>
<div class="plan-features">
<div class="feature-item">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>50 team members</span>
</div>
<div class="feature-item">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>20 bots</span>
</div>
<div class="feature-item">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>50 GB storage</span>
</div>
<div class="feature-item">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>500,000 API calls/month</span>
</div>
</div>
<div class="plan-renewal">
<span class="renewal-label">Next billing date:</span>
<span class="renewal-date" id="nextBillingDate">February 21, 2025</span>
</div>
</div>
<div class="plan-actions">
<button class="btn-primary" onclick="showUpgradeModal()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
Upgrade Plan
</button>
<button class="btn-secondary" onclick="showCancelModal()">Cancel Subscription</button>
</div>
</div>
<div class="billing-card usage-summary">
<div class="card-header">
<h2>Usage This Period</h2>
<span class="period-label">Jan 21 - Feb 21, 2025</span>
</div>
<div class="usage-items" hx-get="/api/billing/usage" hx-trigger="load" hx-swap="innerHTML">
<div class="usage-item">
<div class="usage-info">
<span class="usage-label">Storage</span>
<span class="usage-value">12.5 GB / 50 GB</span>
</div>
<div class="usage-bar">
<div class="usage-fill" style="width: 25%"></div>
</div>
</div>
<div class="usage-item">
<div class="usage-info">
<span class="usage-label">API Calls</span>
<span class="usage-value">125,000 / 500,000</span>
</div>
<div class="usage-bar">
<div class="usage-fill" style="width: 25%"></div>
</div>
</div>
<div class="usage-item">
<div class="usage-info">
<span class="usage-label">Messages</span>
<span class="usage-value">45,000 / 100,000</span>
</div>
<div class="usage-bar">
<div class="usage-fill" style="width: 45%"></div>
</div>
</div>
<div class="usage-item">
<div class="usage-info">
<span class="usage-label">Team Members</span>
<span class="usage-value">12 / 50</span>
</div>
<div class="usage-bar">
<div class="usage-fill" style="width: 24%"></div>
</div>
</div>
</div>
</div>
<div class="billing-card payment-method">
<div class="card-header">
<h2>Payment Method</h2>
<button class="btn-icon" onclick="showAddPaymentModal()">
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
</button>
</div>
<div class="payment-methods" hx-get="/api/billing/payment-methods" hx-trigger="load" hx-swap="innerHTML">
<div class="payment-card active">
<div class="card-brand">
<svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="1.5" fill="none">
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>
<line x1="1" y1="10" x2="23" y2="10"></line>
</svg>
</div>
<div class="card-details">
<span class="card-number">•••• •••• •••• 4242</span>
<span class="card-expiry">Expires 12/2026</span>
</div>
<span class="default-badge">Default</span>
</div>
</div>
</div>
<div class="billing-card billing-address">
<div class="card-header">
<h2>Billing Address</h2>
<button class="btn-icon" onclick="showEditAddressModal()">
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
</svg>
</button>
</div>
<div class="address-content" hx-get="/api/billing/address" hx-trigger="load" hx-swap="innerHTML">
<p class="address-name">Acme Corporation</p>
<p class="address-line">123 Business Street</p>
<p class="address-line">Suite 456</p>
<p class="address-line">San Francisco, CA 94102</p>
<p class="address-line">United States</p>
</div>
</div>
</div>
<div class="billing-section">
<div class="section-header">
<h2>Billing History</h2>
<button class="btn-secondary" onclick="exportInvoices()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Export All
</button>
</div>
<div class="billing-table-container">
<table class="billing-table" hx-get="/api/billing/invoices" hx-trigger="load" hx-swap="innerHTML">
<thead>
<tr>
<th>Invoice</th>
<th>Date</th>
<th>Amount</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<span class="invoice-number">INV-2025-0001</span>
</td>
<td>Jan 21, 2025</td>
<td>$49.00</td>
<td><span class="status-badge paid">Paid</span></td>
<td>
<button class="btn-icon" title="Download Invoice">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<button class="btn-icon" title="View Details">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</td>
</tr>
<tr>
<td>
<span class="invoice-number">INV-2024-0012</span>
</td>
<td>Dec 21, 2024</td>
<td>$49.00</td>
<td><span class="status-badge paid">Paid</span></td>
<td>
<button class="btn-icon" title="Download Invoice">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<button class="btn-icon" title="View Details">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</td>
</tr>
<tr>
<td>
<span class="invoice-number">INV-2024-0011</span>
</td>
<td>Nov 21, 2024</td>
<td>$49.00</td>
<td><span class="status-badge paid">Paid</span></td>
<td>
<button class="btn-icon" title="Download Invoice">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<button class="btn-icon" title="View Details">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
<dialog id="upgradeModal" class="billing-modal">
<div class="modal-content">
<div class="modal-header">
<h2>Upgrade Your Plan</h2>
<button class="btn-close" onclick="closeModal('upgradeModal')">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="plan-grid">
<div class="plan-option">
<div class="plan-name">Pro</div>
<div class="plan-price">$49<span>/mo</span></div>
<div class="plan-current">Current Plan</div>
</div>
<div class="plan-option recommended">
<div class="recommended-badge">Recommended</div>
<div class="plan-name">Business</div>
<div class="plan-price">$99<span>/mo</span></div>
<ul class="plan-highlights">
<li>200 team members</li>
<li>100 bots</li>
<li>200 GB storage</li>
<li>Priority support</li>
</ul>
<button class="btn-primary" hx-post="/api/billing/upgrade" hx-vals='{"plan": "business"}'>
Upgrade to Business
</button>
</div>
<div class="plan-option">
<div class="plan-name">Enterprise</div>
<div class="plan-price">Custom</div>
<ul class="plan-highlights">
<li>Unlimited everything</li>
<li>Dedicated support</li>
<li>Custom integrations</li>
<li>SLA guarantee</li>
</ul>
<button class="btn-secondary" onclick="contactSales()">Contact Sales</button>
</div>
</div>
<div class="proration-notice">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
<span>Your next invoice will be prorated based on your remaining billing period.</span>
</div>
</div>
</div>
</dialog>
<dialog id="cancelModal" class="billing-modal">
<div class="modal-content">
<div class="modal-header">
<h2>Cancel Subscription</h2>
<button class="btn-close" onclick="closeModal('cancelModal')">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="cancel-warning">
<svg viewBox="0 0 24 24" width="48" height="48" stroke="currentColor" stroke-width="1.5" fill="none">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<h3>We're sorry to see you go!</h3>
<p>Before you cancel, would you like to consider any of these options?</p>
</div>
<div class="retention-offers" hx-get="/api/billing/retention-offers" hx-trigger="load" hx-swap="innerHTML">
<div class="retention-offer">
<div class="offer-icon">💰</div>
<div class="offer-details">
<h4>Get 25% off for 3 months</h4>
<p>Stay with us at a reduced rate</p>
</div>
<button class="btn-secondary" hx-post="/api/billing/accept-offer" hx-vals='{"offer": "discount_25"}'>
Accept Offer
</button>
</div>
<div class="retention-offer">
<div class="offer-icon">⏸️</div>
<div class="offer-details">
<h4>Pause your subscription</h4>
<p>Take a break for up to 3 months</p>
</div>
<button class="btn-secondary" hx-post="/api/billing/pause">
Pause Instead
</button>
</div>
<div class="retention-offer">
<div class="offer-icon">📉</div>
<div class="offer-details">
<h4>Downgrade to a smaller plan</h4>
<p>Keep essential features at a lower cost</p>
</div>
<button class="btn-secondary" onclick="showDowngradeOptions()">
View Plans
</button>
</div>
</div>
<form class="cancel-form" hx-post="/api/billing/cancel" hx-confirm="Are you sure you want to cancel your subscription?">
<div class="form-group">
<label>Why are you cancelling?</label>
<select name="reason" required>
<option value="">Select a reason...</option>
<option value="too_expensive">Too expensive</option>
<option value="missing_features">Missing features I need</option>
<option value="not_using">Not using it enough</option>
<option value="switching">Switching to another product</option>
<option value="temporary">Temporary - will be back</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label>Additional feedback (optional)</label>
<textarea name="feedback" rows="3" placeholder="Help us improve..."></textarea>
</div>
<div class="cancel-actions">
<button type="button" class="btn-secondary" onclick="closeModal('cancelModal')">Keep Subscription</button>
<button type="submit" class="btn-danger">Cancel Subscription</button>
</div>
</form>
</div>
</div>
</dialog>
<style>
.billing-dashboard {
padding: 24px;
max-width: 1400px;
margin: 0 auto;
}
.billing-header {
margin-bottom: 32px;
}
.billing-header h1 {
font-size: 28px;
font-weight: 600;
margin: 0 0 8px 0;
color: var(--text-primary);
}
.billing-header p {
color: var(--text-secondary);
margin: 0;
}
.billing-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
margin-bottom: 32px;
}
.billing-card {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h2 {
font-size: 18px;
font-weight: 600;
margin: 0;
color: var(--text-primary);
}
.plan-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.plan-badge.pro {
background: var(--accent-color);
color: white;
}
.plan-price {
margin-bottom: 20px;
}
.plan-price .amount {
font-size: 48px;
font-weight: 700;
color: var(--text-primary);
}
.plan-price .period {
font-size: 16px;
color: var(--text-secondary);
}
.plan-features {
margin-bottom: 20px;
}
.feature-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 0;
color: var(--text-secondary);
}
.feature-item svg {
color: var(--success-color);
}
.plan-renewal {
padding: 16px;
background: var(--bg-secondary);
border-radius: 8px;
margin-bottom: 20px;
}
.renewal-label {
color: var(--text-secondary);
font-size: 14px;
}
.renewal-date {
color: var(--text-primary);
font-weight: 600;
margin-left: 8px;
}
.plan-actions {
display: flex;
gap: 12px;
}
.usage-item {
margin-bottom: 16px;
}
.usage-item:last-child {
margin-bottom: 0;
}
.usage-info {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.usage-label {
color: var(--text-secondary);
font-size: 14px;
}
.usage-value {
color: var(--text-primary);
font-size: 14px;
font-weight: 500;
}
.usage-bar {
height: 8px;
background: var(--bg-secondary);
border-radius: 4px;
overflow: hidden;
}
.usage-fill {
height: 100%;
background: var(--accent-color);
border-radius: 4px;
transition: width 0.3s ease;
}
.usage-fill.warning {
background: var(--warning-color);
}
.usage-fill.danger {
background: var(--danger-color);
}
.payment-card {
display: flex;
align-items: center;
gap: 16px;
padding: 16px;
background: var(--bg-secondary);
border-radius: 8px;
border: 2px solid transparent;
}
.payment-card.active {
border-color: var(--accent-color);
}
.card-brand svg {
color: var(--text-secondary);
}
.card-details {
flex: 1;
}
.card-number {
display: block;
font-weight: 600;
color: var(--text-primary);
}
.card-expiry {
font-size: 12px;
color: var(--text-secondary);
}
.default-badge {
font-size: 11px;
padding: 4px 8px;
background: var(--accent-color);
color: white;
border-radius: 4px;
}
.address-content p {
margin: 0 0 4px 0;
color: var(--text-secondary);
}
.address-name {
font-weight: 600;
color: var(--text-primary) !important;
}
.billing-section {
background: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 24px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.section-header h2 {
font-size: 18px;
font-weight: 600;
margin: 0;
color: var(--text-primary);
}
.billing-table {
width: 100%;
border-collapse: collapse;
}
.billing-table th,
.billing-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid var(--border-color);
}
.billing-table th {
font-weight: 600;
color: var(--text-secondary);
font-size: 12px;
text-transform: uppercase;
}
.billing-table td {
color: var(--text-primary);
}
.invoice-number {
font-family: monospace;
font-weight: 500;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-badge.paid {
background: var(--success-bg);
color: var(--success-color);
}
.status-badge.pending {
background: var(--warning-bg);
color: var(--warning-color);
}
.status-badge.failed {
background: var(--danger-bg);
color: var(--danger-color);
}
.btn-primary {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--accent-color);
color: white;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-primary:hover {
background: var(--accent-hover);
}
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
.btn-secondary:hover {
background: var(--bg-hover);
}
.btn-danger {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--danger-color);
color: white;
border: none;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
}
.btn-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: none;
border: none;
border-radius: 6px;
cursor: pointer;
color: var(--text-secondary);
transition: background 0.2s;
}
.btn-icon:hover {
background: var(--bg-secondary);
color: var(--text-primary);
}
.billing-modal {
padding: 0;
border: none;
border-radius: 16px;
max-width: 800px;
width: 90%;
background: var(--card-bg);
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.billing-modal::backdrop {
background: rgba(0, 0, 0, 0.5);
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24px;
border-bottom: 1px solid var(--border-color);
}
.modal-header h2 {
margin: 0;
font-size: 20px;
color: var(--text-primary);
}
.btn-close {
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary);
padding: 4px;
border-radius: 4px;

View file

@ -0,0 +1,455 @@
<section class="compliance-dashboard">
<div class="dashboard-header">
<div class="header-title">
<h1>Compliance Dashboard</h1>
<p>SOC 2 Type II compliance status, controls monitoring, and audit evidence</p>
</div>
<div class="header-actions">
<select class="framework-selector" id="complianceFramework" onchange="updateFramework(this.value)">
<option value="soc2">SOC 2 Type II</option>
<option value="gdpr">GDPR</option>
<option value="hipaa">HIPAA</option>
<option value="iso27001">ISO 27001</option>
</select>
<button class="btn-secondary" onclick="generateComplianceReport()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
Generate Report
</button>
<button class="btn-primary" onclick="startAuditPrep()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M9 11l3 3L22 4"></path>
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
</svg>
Audit Prep
</button>
</div>
</div>
<div class="compliance-overview" hx-get="/api/compliance/dashboard/overview" hx-trigger="load" hx-swap="innerHTML">
<div class="overview-card overall-score">
<div class="score-ring">
<svg viewBox="0 0 100 100" class="score-svg">
<circle cx="50" cy="50" r="45" fill="none" stroke="var(--surface-border)" stroke-width="8"></circle>
<circle cx="50" cy="50" r="45" fill="none" stroke="var(--success)" stroke-width="8" stroke-dasharray="254.47" stroke-dashoffset="25.45" transform="rotate(-90 50 50)" stroke-linecap="round"></circle>
</svg>
<div class="score-value">
<span class="score-number">91%</span>
<span class="score-label">Compliant</span>
</div>
</div>
<div class="score-details">
<span class="score-title">Overall Compliance Score</span>
<span class="score-change positive">+3% from last assessment</span>
</div>
</div>
<div class="overview-card controls-status">
<div class="status-icon healthy">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
<polyline points="9 12 12 15 16 10"></polyline>
</svg>
</div>
<div class="status-content">
<span class="status-value">156 / 171</span>
<span class="status-label">Controls Passing</span>
<span class="status-detail">15 controls need attention</span>
</div>
</div>
<div class="overview-card evidence-status">
<div class="status-icon warning">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="12" y1="18" x2="12" y2="12"></line>
<line x1="12" y1="9" x2="12.01" y2="9"></line>
</svg>
</div>
<div class="status-content">
<span class="status-value">12</span>
<span class="status-label">Evidence Items Due</span>
<span class="status-detail warning">5 due within 7 days</span>
</div>
</div>
<div class="overview-card audit-status">
<div class="status-icon info">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="2" fill="none">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
<line x1="16" y1="2" x2="16" y2="6"></line>
<line x1="8" y1="2" x2="8" y2="6"></line>
<line x1="3" y1="10" x2="21" y2="10"></line>
</svg>
</div>
<div class="status-content">
<span class="status-value">45 Days</span>
<span class="status-label">Until Next Audit</span>
<span class="status-detail">Scheduled: March 7, 2025</span>
</div>
</div>
</div>
<div class="dashboard-grid">
<div class="card tsc-controls">
<div class="card-header">
<h2>Trust Service Criteria</h2>
<div class="tsc-legend">
<span class="legend-item passing"><span class="dot"></span> Passing</span>
<span class="legend-item failing"><span class="dot"></span> Failing</span>
<span class="legend-item pending"><span class="dot"></span> Pending</span>
</div>
</div>
<div class="tsc-grid" hx-get="/api/compliance/dashboard/tsc" hx-trigger="load" hx-swap="innerHTML">
<div class="tsc-category" onclick="showTscDetails('security')">
<div class="tsc-header">
<div class="tsc-icon security">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
</div>
<span class="tsc-name">Security</span>
</div>
<div class="tsc-progress">
<div class="tsc-bar">
<div class="tsc-fill passing" style="width: 94%"></div>
</div>
<span class="tsc-percent">94%</span>
</div>
<div class="tsc-counts">
<span class="passing">47 passing</span>
<span class="failing">3 failing</span>
</div>
</div>
<div class="tsc-category" onclick="showTscDetails('availability')">
<div class="tsc-header">
<div class="tsc-icon availability">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
</svg>
</div>
<span class="tsc-name">Availability</span>
</div>
<div class="tsc-progress">
<div class="tsc-bar">
<div class="tsc-fill passing" style="width: 100%"></div>
</div>
<span class="tsc-percent">100%</span>
</div>
<div class="tsc-counts">
<span class="passing">28 passing</span>
<span class="failing">0 failing</span>
</div>
</div>
<div class="tsc-category" onclick="showTscDetails('processing')">
<div class="tsc-header">
<div class="tsc-icon processing">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</div>
<span class="tsc-name">Processing Integrity</span>
</div>
<div class="tsc-progress">
<div class="tsc-bar">
<div class="tsc-fill passing" style="width: 88%"></div>
</div>
<span class="tsc-percent">88%</span>
</div>
<div class="tsc-counts">
<span class="passing">22 passing</span>
<span class="failing">3 failing</span>
</div>
</div>
<div class="tsc-category" onclick="showTscDetails('confidentiality')">
<div class="tsc-header">
<div class="tsc-icon confidentiality">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</div>
<span class="tsc-name">Confidentiality</span>
</div>
<div class="tsc-progress">
<div class="tsc-bar">
<div class="tsc-fill passing" style="width: 91%"></div>
</div>
<span class="tsc-percent">91%</span>
</div>
<div class="tsc-counts">
<span class="passing">32 passing</span>
<span class="failing">3 failing</span>
</div>
</div>
<div class="tsc-category" onclick="showTscDetails('privacy')">
<div class="tsc-header">
<div class="tsc-icon privacy">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</div>
<span class="tsc-name">Privacy</span>
</div>
<div class="tsc-progress">
<div class="tsc-bar">
<div class="tsc-fill passing" style="width: 82%"></div>
</div>
<span class="tsc-percent">82%</span>
</div>
<div class="tsc-counts">
<span class="passing">27 passing</span>
<span class="failing">6 failing</span>
</div>
</div>
</div>
</div>
<div class="card failing-controls">
<div class="card-header">
<h2>Controls Requiring Attention</h2>
<select class="severity-filter" id="controlSeverity" onchange="filterControls(this.value)">
<option value="all">All Severities</option>
<option value="critical">Critical</option>
<option value="high">High</option>
<option value="medium">Medium</option>
<option value="low">Low</option>
</select>
</div>
<div class="controls-list" hx-get="/api/compliance/dashboard/failing-controls" hx-trigger="load" hx-swap="innerHTML">
<div class="control-item critical">
<div class="control-severity critical">Critical</div>
<div class="control-info">
<span class="control-id">CC6.1.3</span>
<span class="control-name">Multi-factor authentication for privileged accounts</span>
<span class="control-category">Security</span>
</div>
<div class="control-details">
<span class="control-issue">2 admin accounts missing MFA</span>
<span class="control-deadline warning">Due: Jan 25, 2025</span>
</div>
<button class="btn-secondary btn-sm" onclick="showControlRemediation('CC6.1.3')">Remediate</button>
</div>
<div class="control-item high">
<div class="control-severity high">High</div>
<div class="control-info">
<span class="control-id">CC7.2.1</span>
<span class="control-name">Security incident response plan testing</span>
<span class="control-category">Security</span>
</div>
<div class="control-details">
<span class="control-issue">Annual test overdue by 15 days</span>
<span class="control-deadline error">Overdue</span>
</div>
<button class="btn-secondary btn-sm" onclick="showControlRemediation('CC7.2.1')">Remediate</button>
</div>
<div class="control-item high">
<div class="control-severity high">High</div>
<div class="control-info">
<span class="control-id">PI1.3.2</span>
<span class="control-name">Data processing agreement documentation</span>
<span class="control-category">Processing Integrity</span>
</div>
<div class="control-details">
<span class="control-issue">3 vendors missing DPAs</span>
<span class="control-deadline warning">Due: Feb 1, 2025</span>
</div>
<button class="btn-secondary btn-sm" onclick="showControlRemediation('PI1.3.2')">Remediate</button>
</div>
<div class="control-item medium">
<div class="control-severity medium">Medium</div>
<div class="control-info">
<span class="control-id">C1.2.4</span>
<span class="control-name">Data classification policy review</span>
<span class="control-category">Confidentiality</span>
</div>
<div class="control-details">
<span class="control-issue">Policy review pending approval</span>
<span class="control-deadline">Due: Feb 15, 2025</span>
</div>
<button class="btn-secondary btn-sm" onclick="showControlRemediation('C1.2.4')">Remediate</button>
</div>
<div class="control-item medium">
<div class="control-severity medium">Medium</div>
<div class="control-info">
<span class="control-id">P3.1.1</span>
<span class="control-name">Privacy notice updates</span>
<span class="control-category">Privacy</span>
</div>
<div class="control-details">
<span class="control-issue">Privacy policy needs update for new data collection</span>
<span class="control-deadline">Due: Feb 28, 2025</span>
</div>
<button class="btn-secondary btn-sm" onclick="showControlRemediation('P3.1.1')">Remediate</button>
</div>
</div>
</div>
<div class="card evidence-collection">
<div class="card-header">
<h2>Evidence Collection</h2>
<button class="btn-secondary btn-sm" onclick="showEvidenceUpload()">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
Upload Evidence
</button>
</div>
<div class="evidence-summary">
<div class="evidence-stat">
<span class="stat-number">342</span>
<span class="stat-label">Total Items</span>
</div>
<div class="evidence-stat">
<span class="stat-number success">318</span>
<span class="stat-label">Current</span>
</div>
<div class="evidence-stat">
<span class="stat-number warning">12</span>
<span class="stat-label">Due Soon</span>
</div>
<div class="evidence-stat">
<span class="stat-number error">12</span>
<span class="stat-label">Overdue</span>
</div>
</div>
<div class="evidence-list" hx-get="/api/compliance/dashboard/evidence" hx-trigger="load" hx-swap="innerHTML">
<div class="evidence-item overdue">
<div class="evidence-icon">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div class="evidence-info">
<span class="evidence-name">Penetration Test Report Q4 2024</span>
<span class="evidence-control">CC4.1.1 - Vulnerability Assessment</span>
</div>
<span class="evidence-status overdue">Overdue</span>
<button class="btn-icon" onclick="uploadEvidence('pen-test-q4')">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
</button>
</div>
<div class="evidence-item due-soon">
<div class="evidence-icon">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div class="evidence-info">
<span class="evidence-name">Access Review Report - January 2025</span>
<span class="evidence-control">CC6.2.1 - Access Control</span>
</div>
<span class="evidence-status due-soon">Due in 3 days</span>
<button class="btn-icon" onclick="uploadEvidence('access-review-jan')">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
</button>
</div>
<div class="evidence-item due-soon">
<div class="evidence-icon">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div class="evidence-info">
<span class="evidence-name">Backup Restoration Test Results</span>
<span class="evidence-control">A1.2.3 - Backup & Recovery</span>
</div>
<span class="evidence-status due-soon">Due in 5 days</span>
<button class="btn-icon" onclick="uploadEvidence('backup-test')">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
</button>
</div>
<div class="evidence-item current">
<div class="evidence-icon">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
</div>
<div class="evidence-info">
<span class="evidence-name">Security Awareness Training Records</span>
<span class="evidence-control">CC1.4.1 - Security Training</span>
</div>
<span class="evidence-status current">Current</span>
<button class="btn-icon" onclick="viewEvidence('security-training')">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
</button>
</div>
</div>
<a href="/admin/compliance/evidence" class="view-all-link">View All Evidence</a>
</div>
<div class="card audit-log">
<div class="card-header">
<h2>Audit Log</h2>
<div class="log-filters">
<select class="log-filter" id="logCategory" onchange="filterLogs()">
<option value="all">All Categories</option>
<option value="access">Access Events</option>
<option value="changes">Configuration Changes</option>
<option value="security">Security Events</option>
<option value="compliance">Compliance Events</option>
</select>
<button class="btn-icon" onclick="exportAuditLog()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
</div>
</div>
<div class="log-list" hx-get="/api/compliance/dashboard/audit-log" hx-trigger="load" hx-swap="innerHTML">
<div class="log-item security">
<div class="log-icon security">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
</div>
<div class="log-content">
<span class="log-action">Failed login attempt</span>
<span class="log-details">User: admin@company.com | IP: 192.168.1.105 | Reason: Invalid password</span>
</div>
<span class="log-time">2 minutes ago</span>
</div>
<div class="log-item access">
<div class="log-icon access">
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none">
<path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path>
<polyline points="10 17 15 12 10 7"></polyline>
<line x1="15" y1="12" x2="3" y2="12"></line>
</svg>
</div>
<div class="log-content">
<span class="log-action">User logged in</span>
<span class="log-details">User: sarah.johnson@company.com | IP: 10.0.0.45</span>
</div>
<span class="log-time">15 minutes ago</span>
</div>
<div class="log-item changes">
<div class="log-icon changes

1116
ui/suite/admin/contacts.html Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,555 @@
<section class="onboarding-wizard">
<div class="wizard-container">
<div class="wizard-sidebar">
<div class="wizard-logo">
<img src="/assets/icons/gb-logo.svg" alt="General Bots" width="48" height="48">
</div>
<div class="wizard-steps">
<div class="wizard-step active" data-step="1">
<div class="step-indicator">
<span class="step-number">1</span>
<svg class="step-check" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="3" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<div class="step-info">
<span class="step-title">Welcome</span>
<span class="step-desc">Tell us about yourself</span>
</div>
</div>
<div class="wizard-step" data-step="2">
<div class="step-indicator">
<span class="step-number">2</span>
<svg class="step-check" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="3" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<div class="step-info">
<span class="step-title">Organization</span>
<span class="step-desc">Create your workspace</span>
</div>
</div>
<div class="wizard-step" data-step="3">
<div class="step-indicator">
<span class="step-number">3</span>
<svg class="step-check" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="3" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<div class="step-info">
<span class="step-title">Choose Plan</span>
<span class="step-desc">Select your subscription</span>
</div>
</div>
<div class="wizard-step" data-step="4">
<div class="step-indicator">
<span class="step-number">4</span>
<svg class="step-check" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="3" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<div class="step-info">
<span class="step-title">Payment</span>
<span class="step-desc">Add payment method</span>
</div>
</div>
<div class="wizard-step" data-step="5">
<div class="step-indicator">
<span class="step-number">5</span>
<svg class="step-check" viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="3" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</div>
<div class="step-info">
<span class="step-title">Get Started</span>
<span class="step-desc">Create your first bot</span>
</div>
</div>
</div>
<div class="wizard-help">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<span>Need help? <a href="/docs/getting-started">View guide</a></span>
</div>
</div>
<div class="wizard-content">
<div class="wizard-panel active" data-panel="1">
<div class="panel-header">
<h1>Welcome to General Bots!</h1>
<p>Let's get you set up in just a few minutes. First, tell us a bit about yourself.</p>
</div>
<form class="panel-form" id="welcomeForm">
<div class="form-group">
<label for="fullName">Full Name</label>
<input type="text" id="fullName" name="full_name" required placeholder="John Smith">
</div>
<div class="form-group">
<label for="role">Your Role</label>
<select id="role" name="role" required>
<option value="">Select your role...</option>
<option value="developer">Developer / Engineer</option>
<option value="product">Product Manager</option>
<option value="marketing">Marketing</option>
<option value="sales">Sales</option>
<option value="support">Customer Support</option>
<option value="executive">Executive / Founder</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label for="useCase">What do you want to build?</label>
<select id="useCase" name="use_case" required>
<option value="">Select primary use case...</option>
<option value="customer_support">Customer Support Bot</option>
<option value="sales_assistant">Sales Assistant</option>
<option value="internal_kb">Internal Knowledge Base</option>
<option value="lead_gen">Lead Generation</option>
<option value="onboarding">Employee Onboarding</option>
<option value="ecommerce">E-commerce Assistant</option>
<option value="other">Something else</option>
</select>
</div>
<div class="form-group">
<label>How did you hear about us?</label>
<div class="radio-group">
<label class="radio-option">
<input type="radio" name="source" value="search">
<span>Search Engine</span>
</label>
<label class="radio-option">
<input type="radio" name="source" value="social">
<span>Social Media</span>
</label>
<label class="radio-option">
<input type="radio" name="source" value="referral">
<span>Friend / Colleague</span>
</label>
<label class="radio-option">
<input type="radio" name="source" value="other">
<span>Other</span>
</label>
</div>
</div>
</form>
<div class="panel-actions">
<div></div>
<button class="btn-primary" onclick="nextStep(1)">
Continue
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</button>
</div>
</div>
<div class="wizard-panel" data-panel="2">
<div class="panel-header">
<h1>Create Your Organization</h1>
<p>Your organization is where you'll manage bots, team members, and knowledge bases.</p>
</div>
<form class="panel-form" id="orgForm">
<div class="form-group">
<label for="orgName">Organization Name</label>
<input type="text" id="orgName" name="org_name" required placeholder="Acme Corporation">
<span class="form-hint">This will be visible to your team members</span>
</div>
<div class="form-group">
<label for="orgSlug">Organization URL</label>
<div class="input-with-prefix">
<span class="input-prefix">app.generalbots.com/</span>
<input type="text" id="orgSlug" name="org_slug" required placeholder="acme" pattern="[a-z0-9-]+">
</div>
<span class="form-hint">Only lowercase letters, numbers, and hyphens</span>
</div>
<div class="form-group">
<label for="teamSize">Team Size</label>
<select id="teamSize" name="team_size" required>
<option value="">How many people will use this?</option>
<option value="1">Just me</option>
<option value="2-5">2-5 people</option>
<option value="6-20">6-20 people</option>
<option value="21-50">21-50 people</option>
<option value="51+">50+ people</option>
</select>
</div>
<div class="form-group">
<label for="industry">Industry</label>
<select id="industry" name="industry">
<option value="">Select your industry (optional)</option>
<option value="technology">Technology</option>
<option value="healthcare">Healthcare</option>
<option value="finance">Finance / Banking</option>
<option value="retail">Retail / E-commerce</option>
<option value="education">Education</option>
<option value="manufacturing">Manufacturing</option>
<option value="consulting">Consulting / Services</option>
<option value="government">Government</option>
<option value="nonprofit">Non-profit</option>
<option value="other">Other</option>
</select>
</div>
</form>
<div class="panel-actions">
<button class="btn-secondary" onclick="prevStep(2)">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back
</button>
<button class="btn-primary" onclick="nextStep(2)">
Continue
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</button>
</div>
</div>
<div class="wizard-panel" data-panel="3">
<div class="panel-header">
<h1>Choose Your Plan</h1>
<p>Select the plan that best fits your needs. You can upgrade or downgrade anytime.</p>
</div>
<div class="billing-toggle">
<span class="toggle-label">Monthly</span>
<label class="toggle-switch">
<input type="checkbox" id="annualToggle" onchange="toggleBilling()">
<span class="toggle-slider"></span>
</label>
<span class="toggle-label">Annual <span class="save-badge">Save 20%</span></span>
</div>
<div class="plans-grid">
<div class="plan-card" data-plan="free">
<div class="plan-header">
<h3>Free</h3>
<p>For individuals and small projects</p>
</div>
<div class="plan-price">
<span class="price-amount">$0</span>
<span class="price-period">/month</span>
</div>
<ul class="plan-features">
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
5 team members
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
2 bots
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
1 GB storage
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
1,000 messages/month
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Community support
</li>
</ul>
<button class="btn-plan" onclick="selectPlan('free')">Select Free</button>
</div>
<div class="plan-card popular" data-plan="pro">
<div class="popular-badge">Most Popular</div>
<div class="plan-header">
<h3>Pro</h3>
<p>For growing teams</p>
</div>
<div class="plan-price">
<span class="price-amount" data-monthly="49" data-annual="39">$49</span>
<span class="price-period">/month</span>
</div>
<ul class="plan-features">
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
50 team members
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
20 bots
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
50 GB storage
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
100,000 messages/month
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Email support
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Custom branding
</li>
</ul>
<button class="btn-plan primary" onclick="selectPlan('pro')">Select Pro</button>
</div>
<div class="plan-card" data-plan="business">
<div class="plan-header">
<h3>Business</h3>
<p>For larger organizations</p>
</div>
<div class="plan-price">
<span class="price-amount" data-monthly="99" data-annual="79">$99</span>
<span class="price-period">/month</span>
</div>
<ul class="plan-features">
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
200 team members
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
100 bots
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
200 GB storage
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Unlimited messages
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Priority support
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
SSO / SAML
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Audit logs
</li>
</ul>
<button class="btn-plan" onclick="selectPlan('business')">Select Business</button>
</div>
<div class="plan-card enterprise" data-plan="enterprise">
<div class="plan-header">
<h3>Enterprise</h3>
<p>For large-scale deployments</p>
</div>
<div class="plan-price">
<span class="price-amount">Custom</span>
<span class="price-period">pricing</span>
</div>
<ul class="plan-features">
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Unlimited everything
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Dedicated support
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Custom integrations
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
SLA guarantee
</li>
<li>
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
On-premise option
</li>
</ul>
<button class="btn-plan" onclick="contactSales()">Contact Sales</button>
</div>
</div>
<div class="panel-actions">
<button class="btn-secondary" onclick="prevStep(3)">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back
</button>
<button class="btn-primary" onclick="nextStep(3)" id="planContinueBtn" disabled>
Continue
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</button>
</div>
</div>
<div class="wizard-panel" data-panel="4">
<div class="panel-header">
<h1>Add Payment Method</h1>
<p>Your card will be charged after the 14-day free trial ends.</p>
</div>
<div class="selected-plan-summary">
<div class="summary-row">
<span>Selected Plan:</span>
<strong id="selectedPlanName">Pro</strong>
</div>
<div class="summary-row">
<span>Price:</span>
<strong id="selectedPlanPrice">$49/month</strong>
</div>
<div class="summary-row trial">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span>14-day free trial - cancel anytime</span>
</div>
</div>
<form class="panel-form payment-form" id="paymentForm">
<div class="form-group">
<label for="cardName">Name on Card</label>
<input type="text" id="cardName" name="card_name" required placeholder="John Smith">
</div>
<div class="form-group">
<label for="cardNumber">Card Number</label>
<div class="card-input-wrapper">
<input type="text" id="cardNumber" name="card_number" required placeholder="1234 5678 9012 3456" maxlength="19">
<div class="card-icons">
<svg viewBox="0 0 24 24" width="24" height="24" stroke="currentColor" stroke-width="1.5" fill="none">
<rect x="1" y="4" width="22" height="16" rx="2" ry="2"></rect>
<line x1="1" y1="10" x2="23" y2="10"></line>
</svg>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="cardExpiry">Expiry Date</label>
<input type="text" id="cardExpiry" name="card_expiry" required placeholder="MM/YY" maxlength="5">
</div>
<div class="form-group">
<label for="cardCvc">CVC</label>
<input type="text" id="cardCvc" name="card_cvc" required placeholder="123" maxlength="4">
</div>
</div>
<div class="form-group">
<label for="billingCountry">Country</label>
<select id="billingCountry" name="billing_country" required>
<option value="">Select country...</option>
<option value="US">United States</option>
<option value="CA">Canada</option>
<option value="GB">United Kingdom</option>
<option value="DE">Germany</option>
<option value="FR">France</option>
<option value="BR">Brazil</option>
<option value="AU">Australia</option>
</select>
</div>
<div class="form-group">
<label for="billingZip">ZIP / Postal Code</label>
<input type="text" id="billingZip" name="billing_zip" required placeholder="12345">
</div>
<div class="secure-notice">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
<span>Your payment information is encrypted and secure</span>
</div>
</form>
<div class="panel-actions">
<button class="btn-secondary" onclick="prevStep(4)">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back
</button>
<button class="btn-primary" onclick="nextStep(4)">
Start Free Trial
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<line x1="5" y1="12" x2="19" y2="12"></line>
<polyline points="12 5 19 12 12 19"></polyline>
</svg>
</button>
</div>
<div class="skip-payment">
<span id="skipPaymentLink" onclick="skipPayment()" style="display: none;">Skip for now (Free plan only)</span>
</div>
</div>
<div class="wizard-panel" data-panel="5">
<div class="panel-header success-header">
<div class="success-icon">
<svg viewBox="0 0 24 24" width="64" height="64" stroke="currentColor" stroke-width="2" fill="none">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"></path>
<polyline points="22 4 12 14.01 9 11.01"></polyline>
</svg>
</div>
<h1>You're All Set!</h1>
<p>Your organization <strong id="finalOrgName">Acme Corporation</strong> has been created.</p>
</div>
<div class="quick-actions">
<h3>What would you like to do first?</h3>
<div class="action-cards">
<a href="/designer" class="action-card">
<div class="action-icon">
<svg viewBox="0 0 24 24" width="32" height="32" stroke="currentColor" stroke-width="1.5" fill="none">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.

View file

@ -0,0 +1,499 @@
<section class="operations-dashboard">
<div class="dashboard-header">
<div class="header-title">
<h1>Operations Dashboard</h1>
<p>Real-time system metrics, distributed tracing, and performance monitoring</p>
</div>
<div class="header-actions">
<div class="time-range-selector">
<button class="range-btn active" data-range="1h">1H</button>
<button class="range-btn" data-range="6h">6H</button>
<button class="range-btn" data-range="24h">24H</button>
<button class="range-btn" data-range="7d">7D</button>
<button class="range-btn" data-range="30d">30D</button>
</div>
<button class="btn-secondary" onclick="toggleAutoRefresh()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
<span id="autoRefreshLabel">Auto-refresh: ON</span>
</button>
<button class="btn-primary" onclick="showAlertConfig()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
Configure Alerts
</button>
</div>
</div>
<div class="system-status-bar" hx-get="/api/ops/status" hx-trigger="load, every 10s" hx-swap="innerHTML">
<div class="status-indicator healthy">
<span class="status-dot"></span>
<span class="status-text">All Systems Operational</span>
</div>
<div class="status-metrics">
<div class="status-metric">
<span class="metric-label">Uptime</span>
<span class="metric-value">99.97%</span>
</div>
<div class="status-metric">
<span class="metric-label">Avg Response</span>
<span class="metric-value">45ms</span>
</div>
<div class="status-metric">
<span class="metric-label">Requests/min</span>
<span class="metric-value">2.4K</span>
</div>
<div class="status-metric">
<span class="metric-label">Error Rate</span>
<span class="metric-value healthy">0.02%</span>
</div>
</div>
</div>
<div class="metrics-grid" hx-get="/api/ops/metrics/summary" hx-trigger="load, every 30s" hx-swap="innerHTML">
<div class="metric-card cpu">
<div class="metric-header">
<div class="metric-icon">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
<line x1="15" y1="1" x2="15" y2="4"></line>
<line x1="9" y1="20" x2="9" y2="23"></line>
<line x1="15" y1="20" x2="15" y2="23"></line>
<line x1="20" y1="9" x2="23" y2="9"></line>
<line x1="20" y1="14" x2="23" y2="14"></line>
<line x1="1" y1="9" x2="4" y2="9"></line>
<line x1="1" y1="14" x2="4" y2="14"></line>
</svg>
</div>
<span class="metric-title">CPU Usage</span>
</div>
<div class="metric-value-large">34%</div>
<div class="metric-sparkline">
<svg viewBox="0 0 100 30" preserveAspectRatio="none" class="sparkline">
<path d="M0,20 L10,18 L20,22 L30,15 L40,17 L50,12 L60,14 L70,10 L80,13 L90,11 L100,9" fill="none" stroke="var(--chart-1)" stroke-width="2"></path>
</svg>
</div>
<div class="metric-footer">
<span class="metric-range">Peak: 67%</span>
<span class="metric-trend positive">-5% from avg</span>
</div>
</div>
<div class="metric-card memory">
<div class="metric-header">
<div class="metric-icon">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<rect x="2" y="6" width="20" height="12" rx="2"></rect>
<line x1="6" y1="10" x2="6" y2="14"></line>
<line x1="10" y1="10" x2="10" y2="14"></line>
<line x1="14" y1="10" x2="14" y2="14"></line>
<line x1="18" y1="10" x2="18" y2="14"></line>
</svg>
</div>
<span class="metric-title">Memory Usage</span>
</div>
<div class="metric-value-large">62%</div>
<div class="metric-sparkline">
<svg viewBox="0 0 100 30" preserveAspectRatio="none" class="sparkline">
<path d="M0,15 L10,14 L20,16 L30,15 L40,18 L50,17 L60,19 L70,18 L80,20 L90,19 L100,18" fill="none" stroke="var(--chart-2)" stroke-width="2"></path>
</svg>
</div>
<div class="metric-footer">
<span class="metric-range">7.4 GB / 12 GB</span>
<span class="metric-trend warning">+8% from avg</span>
</div>
</div>
<div class="metric-card disk">
<div class="metric-header">
<div class="metric-icon">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
</svg>
</div>
<span class="metric-title">Disk I/O</span>
</div>
<div class="metric-value-large">2.1K</div>
<div class="metric-sparkline">
<svg viewBox="0 0 100 30" preserveAspectRatio="none" class="sparkline">
<path d="M0,25 L10,20 L20,22 L30,18 L40,15 L50,17 L60,12 L70,14 L80,10 L90,8 L100,12" fill="none" stroke="var(--chart-3)" stroke-width="2"></path>
</svg>
</div>
<div class="metric-footer">
<span class="metric-range">IOPS (read/write)</span>
<span class="metric-trend positive">Normal</span>
</div>
</div>
<div class="metric-card network">
<div class="metric-header">
<div class="metric-icon">
<svg viewBox="0 0 24 24" width="20" height="20" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
</svg>
</div>
<span class="metric-title">Network</span>
</div>
<div class="metric-value-large">145 Mbps</div>
<div class="metric-sparkline">
<svg viewBox="0 0 100 30" preserveAspectRatio="none" class="sparkline">
<path d="M0,20 L10,15 L20,18 L30,12 L40,14 L50,10 L60,13 L70,8 L80,11 L90,7 L100,10" fill="none" stroke="var(--chart-4)" stroke-width="2"></path>
</svg>
</div>
<div class="metric-footer">
<span class="metric-range">In: 95 / Out: 50</span>
<span class="metric-trend positive">Stable</span>
</div>
</div>
</div>
<div class="dashboard-grid">
<div class="card request-metrics">
<div class="card-header">
<h2>Request Metrics</h2>
<div class="metric-toggles">
<label class="toggle-label">
<input type="checkbox" checked onchange="toggleMetric('requests')">
<span class="toggle-text">Requests</span>
</label>
<label class="toggle-label">
<input type="checkbox" checked onchange="toggleMetric('latency')">
<span class="toggle-text">Latency</span>
</label>
<label class="toggle-label">
<input type="checkbox" onchange="toggleMetric('errors')">
<span class="toggle-text">Errors</span>
</label>
</div>
</div>
<div class="chart-container" id="requestChart" hx-get="/api/ops/metrics/requests" hx-trigger="load, every 30s" hx-swap="innerHTML">
<svg viewBox="0 0 600 200" class="line-chart">
<defs>
<linearGradient id="requestGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:var(--chart-1);stop-opacity:0.3" />
<stop offset="100%" style="stop-color:var(--chart-1);stop-opacity:0" />
</linearGradient>
</defs>
<g class="grid-lines">
<line x1="50" y1="20" x2="580" y2="20" stroke="var(--surface-border)" stroke-dasharray="4"></line>
<line x1="50" y1="65" x2="580" y2="65" stroke="var(--surface-border)" stroke-dasharray="4"></line>
<line x1="50" y1="110" x2="580" y2="110" stroke="var(--surface-border)" stroke-dasharray="4"></line>
<line x1="50" y1="155" x2="580" y2="155" stroke="var(--surface-border)" stroke-dasharray="4"></line>
</g>
<g class="y-axis">
<text x="45" y="25" text-anchor="end" fill="var(--text-secondary)" font-size="10">3K</text>
<text x="45" y="70" text-anchor="end" fill="var(--text-secondary)" font-size="10">2K</text>
<text x="45" y="115" text-anchor="end" fill="var(--text-secondary)" font-size="10">1K</text>
<text x="45" y="160" text-anchor="end" fill="var(--text-secondary)" font-size="10">0</text>
</g>
<path class="area-fill" fill="url(#requestGradient)" d="M50,155 L100,120 L150,100 L200,110 L250,85 L300,90 L350,70 L400,75 L450,60 L500,65 L550,55 L580,50 L580,155 L50,155 Z"></path>
<path class="line-requests" fill="none" stroke="var(--chart-1)" stroke-width="2" d="M50,155 L100,120 L150,100 L200,110 L250,85 L300,90 L350,70 L400,75 L450,60 L500,65 L550,55 L580,50"></path>
<path class="line-latency" fill="none" stroke="var(--chart-2)" stroke-width="2" stroke-dasharray="5,5" d="M50,140 L100,135 L150,130 L200,140 L250,125 L300,130 L350,120 L400,125 L450,115 L500,120 L550,110 L580,115"></path>
<g class="x-axis">
<text x="50" y="180" text-anchor="middle" fill="var(--text-secondary)" font-size="10">12:00</text>
<text x="150" y="180" text-anchor="middle" fill="var(--text-secondary)" font-size="10">12:10</text>
<text x="250" y="180" text-anchor="middle" fill="var(--text-secondary)" font-size="10">12:20</text>
<text x="350" y="180" text-anchor="middle" fill="var(--text-secondary)" font-size="10">12:30</text>
<text x="450" y="180" text-anchor="middle" fill="var(--text-secondary)" font-size="10">12:40</text>
<text x="550" y="180" text-anchor="middle" fill="var(--text-secondary)" font-size="10">12:50</text>
</g>
</svg>
<div class="chart-legend">
<span class="legend-item"><span class="legend-color" style="background: var(--chart-1)"></span>Requests/min</span>
<span class="legend-item"><span class="legend-color" style="background: var(--chart-2)"></span>P95 Latency (ms)</span>
</div>
</div>
</div>
<div class="card distributed-tracing">
<div class="card-header">
<h2>Distributed Tracing</h2>
<div class="trace-controls">
<input type="text" class="trace-search" placeholder="Search trace ID..." id="traceSearch">
<select class="trace-filter" id="traceFilter" onchange="filterTraces(this.value)">
<option value="all">All Traces</option>
<option value="slow">Slow (>500ms)</option>
<option value="errors">Errors Only</option>
<option value="api">API Requests</option>
<option value="db">Database</option>
</select>
</div>
</div>
<div class="trace-list" hx-get="/api/ops/traces" hx-trigger="load, every 60s" hx-swap="innerHTML">
<div class="trace-item" onclick="showTraceDetail('trace_001')">
<div class="trace-status success"></div>
<div class="trace-info">
<span class="trace-name">POST /api/chat/message</span>
<span class="trace-id">trace_a1b2c3d4e5f6</span>
</div>
<div class="trace-spans">
<div class="span-bar" style="width: 100%">
<div class="span-segment api" style="width: 15%" title="API Handler: 45ms"></div>
<div class="span-segment llm" style="width: 65%" title="LLM Call: 195ms"></div>
<div class="span-segment db" style="width: 15%" title="Database: 45ms"></div>
<div class="span-segment other" style="width: 5%" title="Other: 15ms"></div>
</div>
</div>
<span class="trace-duration">300ms</span>
<span class="trace-time">2 min ago</span>
</div>
<div class="trace-item" onclick="showTraceDetail('trace_002')">
<div class="trace-status warning"></div>
<div class="trace-info">
<span class="trace-name">GET /api/kb/search</span>
<span class="trace-id">trace_b2c3d4e5f6g7</span>
</div>
<div class="trace-spans">
<div class="span-bar" style="width: 100%">
<div class="span-segment api" style="width: 5%" title="API Handler: 35ms"></div>
<div class="span-segment vector" style="width: 80%" title="Vector Search: 560ms"></div>
<div class="span-segment db" style="width: 10%" title="Database: 70ms"></div>
<div class="span-segment other" style="width: 5%" title="Other: 35ms"></div>
</div>
</div>
<span class="trace-duration warning">700ms</span>
<span class="trace-time">5 min ago</span>
</div>
<div class="trace-item" onclick="showTraceDetail('trace_003')">
<div class="trace-status error"></div>
<div class="trace-info">
<span class="trace-name">POST /api/billing/checkout</span>
<span class="trace-id">trace_c3d4e5f6g7h8</span>
</div>
<div class="trace-spans">
<div class="span-bar" style="width: 100%">
<div class="span-segment api" style="width: 10%" title="API Handler: 25ms"></div>
<div class="span-segment external" style="width: 70%" title="Stripe API: 175ms (Error)"></div>
<div class="span-segment db" style="width: 15%" title="Database: 37ms"></div>
<div class="span-segment other" style="width: 5%" title="Other: 13ms"></div>
</div>
</div>
<span class="trace-duration error">250ms</span>
<span class="trace-time">8 min ago</span>
</div>
<div class="trace-item" onclick="showTraceDetail('trace_004')">
<div class="trace-status success"></div>
<div class="trace-info">
<span class="trace-name">GET /api/user/profile</span>
<span class="trace-id">trace_d4e5f6g7h8i9</span>
</div>
<div class="trace-spans">
<div class="span-bar" style="width: 100%">
<div class="span-segment api" style="width: 20%" title="API Handler: 10ms"></div>
<div class="span-segment cache" style="width: 30%" title="Cache: 15ms"></div>
<div class="span-segment db" style="width: 40%" title="Database: 20ms"></div>
<div class="span-segment other" style="width: 10%" title="Other: 5ms"></div>
</div>
</div>
<span class="trace-duration">50ms</span>
<span class="trace-time">12 min ago</span>
</div>
</div>
<div class="trace-legend">
<span class="legend-item"><span class="legend-color api"></span>API</span>
<span class="legend-item"><span class="legend-color llm"></span>LLM</span>
<span class="legend-item"><span class="legend-color db"></span>Database</span>
<span class="legend-item"><span class="legend-color vector"></span>Vector DB</span>
<span class="legend-item"><span class="legend-color cache"></span>Cache</span>
<span class="legend-item"><span class="legend-color external"></span>External</span>
</div>
</div>
<div class="card service-health">
<div class="card-header">
<h2>Service Health</h2>
<button class="btn-icon" onclick="refreshHealth()">
<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none">
<polyline points="23 4 23 10 17 10"></polyline>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
</div>
<div class="service-grid" hx-get="/api/ops/services/health" hx-trigger="load, every 15s" hx-swap="innerHTML">
<div class="service-item healthy">
<div class="service-status healthy"></div>
<div class="service-info">
<span class="service-name">API Gateway</span>
<span class="service-details">4 instances | Load: 34%</span>
</div>
<div class="service-metrics">
<span class="metric">2.4K req/min</span>
<span class="metric">45ms avg</span>
</div>
</div>
<div class="service-item healthy">
<div class="service-status healthy"></div>
<div class="service-info">
<span class="service-name">PostgreSQL</span>
<span class="service-details">Primary + 2 Replicas</span>
</div>
<div class="service-metrics">
<span class="metric">450 conn</span>
<span class="metric">12ms avg</span>
</div>
</div>
<div class="service-item healthy">
<div class="service-status healthy"></div>
<div class="service-info">
<span class="service-name">Qdrant (Vector DB)</span>
<span class="service-details">3 nodes cluster</span>
</div>
<div class="service-metrics">
<span class="metric">2.4M vectors</span>
<span class="metric">8ms search</span>
</div>
</div>
<div class="service-item healthy">
<div class="service-status healthy"></div>
<div class="service-info">
<span class="service-name">Redis Cache</span>
<span class="service-details">Cluster mode | 3 shards</span>
</div>
<div class="service-metrics">
<span class="metric">94% hit rate</span>
<span class="metric">0.5ms avg</span>
</div>
</div>
<div class="service-item warning">
<div class="service-status warning"></div>
<div class="service-info">
<span class="service-name">LLM Service</span>
<span class="service-details">OpenAI GPT-4 | High latency</span>
</div>
<div class="service-metrics">
<span class="metric">125K tok/min</span>
<span class="metric warning">1.2s avg</span>
</div>
</div>
<div class="service-item healthy">
<div class="service-status healthy"></div>
<div class="service-info">
<span class="service-name">Object Storage</span>
<span class="service-details">S3 Compatible</span>
</div>
<div class="service-metrics">
<span class="metric">34.5 GB used</span>
<span class="metric">50ms avg</span>
</div>
</div>
</div>
</div>
<div class="card error-tracking">
<div class="card-header">
<h2>Error Tracking</h2>
<a href="/admin/errors" class="view-all-link">View All Errors</a>
</div>
<div class="error-summary">
<div class="error-stat">
<span class="stat-number">23</span>
<span class="stat-label">Total Errors (24h)</span>
</div>
<div class="error-stat">
<span class="stat-number error">5</span>
<span class="stat-label">Critical</span>
</div>
<div class="error-stat">
<span class="stat-number warning">8</span>
<span class="stat-label">Warnings</span>
</div>
<div class="error-stat">
<span class="stat-number">10</span>
<span class="stat-label">Info</span>
</div>
</div>
<div class="error-list" hx-get="/api/ops/errors/recent" hx-trigger="load, every 60s" hx-swap="innerHTML">
<div class="error-item critical">
<div class="error-severity critical">Critical</div>
<div class="error-info">
<span class="error-message">Payment processing failed: Stripe API timeout</span>
<span class="error-location">billing/checkout.rs:145</span>
</div>
<div class="error-meta">
<span class="error-count">3 occurrences</span>
<span class="error-time">15 min ago</span>
</div>
</div>
<div class="error-item warning">
<div class="error-severity warning">Warning</div>
<div class="error-info">
<span class="error-message">Rate limit approaching for OpenAI API</span>
<span class="error-location">llm/openai.rs:89</span>
</div>
<div class="error-meta">
<span class="error-count">12 occurrences</span>
<span class="error-time">30 min ago</span>
</div>
</div>
<div class="error-item warning">
<div class="error-severity warning">Warning</div>
<div class="error-info">
<span class="error-message">Slow database query detected (>1s)</span>
<span class="error-location">core/analytics.rs:234</span>
</div>
<div class="error-meta">
<span class="error-count">5 occurrences</span>
<span class="error-time">45 min ago</span>
</div>
</div>
<div class="error-item info">
<div class="error-severity info">Info</div>
<div class="error-info">
<span class="error-message">Cache miss rate above threshold</span>
<span class="error-location">cache/redis.rs:67</span>
</div>
<div class="error-meta">
<span class="error-count">8 occurrences</span>
<span class="error-time">1 hour ago</span>
</div>
</div>
</div>
</div>
<div class="card endpoint-performance">
<div class="card-header">
<h2>Endpoint Performance</h2>
<select class="sort-selector" id="endpointSort" onchange="sortEndpoints(this.value)">
<option value="requests">By Requests</option>
<option value="latency">By Latency</option>
<option value="errors">By Error Rate</option>
</select>
</div>
<div class="endpoint-list" hx-get="/api/ops/endpoints/performance" hx-trigger="load, every 60s" hx-swap="innerHTML">
<div class="endpoint-item">
<div class="endpoint-info">
<span class="endpoint-method post">POST</span>
<span class="endpoint-path">/api/chat/message</span>
</div>
<div class="endpoint-metrics">
<div class="endpoint-metric">
<span class="metric-value">845</span>
<span class="metric-label">req/min</span>
</div>
<div class="endpoint-metric">
<span class="metric-value">320ms</span>
<span class="metric-label">P95</span>
</div>
<div class="endpoint-metric">
<span class="metric-value healthy">0.1%</span>
<span class="metric-label">errors</span>
</div>
</div>
</div>
<div class="endpoint-item">
<div class="endpoint-info">
<span class="endpoint-method get">GET</span>
<span class="endpoint-path">/api/kb/search</span>
</div>
<div class="endpoint-metrics">
<div class="endpoint-metric">
<span class="metric-value">523</span>
<span class="metric-label">req/min</span>
</div>
<div class="endpoint-metric">
<span class="metric-value warning">580ms

View file

@ -0,0 +1,868 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Organization Settings - General Bots</title>
<link rel="stylesheet" href="/ui/suite/css/common.css">
<link rel="stylesheet" href="/ui/suite/admin/admin.css">
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
.org-settings-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 1px solid var(--border-color, #e0e0e0);
}
.settings-header h1 {
font-size: 1.75rem;
font-weight: 600;
color: var(--text-primary, #1a1a1a);
margin: 0;
}
.settings-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
border-bottom: 1px solid var(--border-color, #e0e0e0);
}
.settings-tab {
padding: 0.75rem 1.5rem;
background: none;
border: none;
cursor: pointer;
font-size: 0.95rem;
color: var(--text-secondary, #666);
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
}
.settings-tab:hover {
color: var(--primary-color, #0066cc);
}
.settings-tab.active {
color: var(--primary-color, #0066cc);
border-bottom-color: var(--primary-color, #0066cc);
font-weight: 500;
}
.settings-panel {
display: none;
}
.settings-panel.active {
display: block;
}
.settings-section {
background: var(--card-bg, #fff);
border-radius: 8px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.settings-section h2 {
font-size: 1.25rem;
font-weight: 600;
margin: 0 0 1rem 0;
color: var(--text-primary, #1a1a1a);
}
.settings-section h3 {
font-size: 1rem;
font-weight: 500;
margin: 1.5rem 0 0.75rem 0;
color: var(--text-secondary, #666);
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--text-primary, #1a1a1a);
}
.form-group .hint {
font-size: 0.85rem;
color: var(--text-secondary, #666);
margin-top: 0.25rem;
}
.form-input {
width: 100%;
padding: 0.75rem;
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 6px;
font-size: 0.95rem;
transition: border-color 0.2s ease;
}
.form-input:focus {
outline: none;
border-color: var(--primary-color, #0066cc);
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
.form-input-group {
display: flex;
gap: 0.5rem;
}
.form-input-group .form-input {
flex: 1;
}
.form-textarea {
min-height: 100px;
resize: vertical;
}
.form-select {
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8L1 3h10z'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
padding-right: 2.5rem;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.toggle-switch {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 6px;
margin-bottom: 0.75rem;
}
.toggle-switch-info {
flex: 1;
}
.toggle-switch-info h4 {
margin: 0 0 0.25rem 0;
font-weight: 500;
color: var(--text-primary, #1a1a1a);
}
.toggle-switch-info p {
margin: 0;
font-size: 0.85rem;
color: var(--text-secondary, #666);
}
.switch {
position: relative;
width: 48px;
height: 26px;
flex-shrink: 0;
margin-left: 1rem;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: 0.3s;
border-radius: 26px;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 3px;
bottom: 3px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--primary-color, #0066cc);
}
input:checked + .slider:before {
transform: translateX(22px);
}
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
.btn-primary {
background: var(--primary-color, #0066cc);
color: white;
}
.btn-primary:hover {
background: var(--primary-hover, #0052a3);
}
.btn-secondary {
background: var(--bg-secondary, #f5f5f5);
color: var(--text-primary, #1a1a1a);
border: 1px solid var(--border-color, #e0e0e0);
}
.btn-secondary:hover {
background: var(--border-color, #e0e0e0);
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn-group {
display: flex;
gap: 0.75rem;
margin-top: 1.5rem;
}
.logo-upload {
display: flex;
align-items: center;
gap: 1.5rem;
}
.logo-preview {
width: 80px;
height: 80px;
border-radius: 8px;
background: var(--bg-secondary, #f5f5f5);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
border: 2px dashed var(--border-color, #e0e0e0);
}
.logo-preview img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.logo-preview-placeholder {
color: var(--text-secondary, #666);
font-size: 2rem;
}
.logo-upload-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.color-picker-group {
display: flex;
align-items: center;
gap: 0.75rem;
}
.color-picker {
width: 50px;
height: 38px;
padding: 0;
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 6px;
cursor: pointer;
}
.color-value {
font-family: monospace;
font-size: 0.9rem;
color: var(--text-secondary, #666);
}
.member-list {
margin-top: 1rem;
}
.member-item {
display: flex;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid var(--border-color, #e0e0e0);
}
.member-item:last-child {
border-bottom: none;
}
.member-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--primary-color, #0066cc);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 1rem;
}
.member-info {
flex: 1;
}
.member-name {
font-weight: 500;
color: var(--text-primary, #1a1a1a);
}
.member-email {
font-size: 0.85rem;
color: var(--text-secondary, #666);
}
.member-role {
padding: 0.25rem 0.75rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 4px;
font-size: 0.85rem;
color: var(--text-secondary, #666);
}
.member-actions {
margin-left: 1rem;
}
.invite-form {
display: flex;
gap: 0.75rem;
margin-top: 1rem;
}
.invite-form .form-input {
flex: 1;
}
.invite-form .form-select {
width: 150px;
}
.pending-invites {
margin-top: 1.5rem;
}
.pending-invite {
display: flex;
align-items: center;
padding: 0.75rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 6px;
margin-bottom: 0.5rem;
}
.pending-invite-email {
flex: 1;
font-size: 0.95rem;
}
.pending-invite-status {
padding: 0.25rem 0.75rem;
background: #fff3cd;
color: #856404;
border-radius: 4px;
font-size: 0.8rem;
margin-right: 1rem;
}
.danger-zone {
border: 1px solid #dc3545;
background: #fff5f5;
}
.danger-zone h2 {
color: #dc3545;
}
.danger-action {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: white;
border: 1px solid #dc3545;
border-radius: 6px;
margin-bottom: 0.75rem;
}
.danger-action:last-child {
margin-bottom: 0;
}
.danger-action-info h4 {
margin: 0 0 0.25rem 0;
color: #dc3545;
}
.danger-action-info p {
margin: 0;
font-size: 0.85rem;
color: var(--text-secondary, #666);
}
.quota-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.quota-item {
padding: 1rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 6px;
}
.quota-item-label {
font-size: 0.85rem;
color: var(--text-secondary, #666);
margin-bottom: 0.5rem;
}
.quota-item-value {
font-size: 1.5rem;
font-weight: 600;
color: var(--text-primary, #1a1a1a);
}
.quota-item-limit {
font-size: 0.85rem;
color: var(--text-secondary, #666);
}
.quota-bar {
height: 6px;
background: #e0e0e0;
border-radius: 3px;
margin-top: 0.5rem;
overflow: hidden;
}
.quota-bar-fill {
height: 100%;
background: var(--primary-color, #0066cc);
border-radius: 3px;
transition: width 0.3s ease;
}
.quota-bar-fill.warning {
background: #ffc107;
}
.quota-bar-fill.danger {
background: #dc3545;
}
.api-key-item {
display: flex;
align-items: center;
padding: 1rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 6px;
margin-bottom: 0.75rem;
}
.api-key-info {
flex: 1;
}
.api-key-name {
font-weight: 500;
color: var(--text-primary, #1a1a1a);
}
.api-key-value {
font-family: monospace;
font-size: 0.85rem;
color: var(--text-secondary, #666);
margin-top: 0.25rem;
}
.api-key-created {
font-size: 0.8rem;
color: var(--text-secondary, #666);
margin-top: 0.25rem;
}
.api-key-actions {
display: flex;
gap: 0.5rem;
}
.btn-icon {
padding: 0.5rem;
background: none;
border: none;
cursor: pointer;
color: var(--text-secondary, #666);
border-radius: 4px;
transition: all 0.2s ease;
}
.btn-icon:hover {
background: var(--border-color, #e0e0e0);
color: var(--text-primary, #1a1a1a);
}
.btn-icon.danger:hover {
background: #ffebee;
color: #dc3545;
}
.webhook-item {
display: flex;
align-items: center;
padding: 1rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 6px;
margin-bottom: 0.75rem;
}
.webhook-info {
flex: 1;
}
.webhook-url {
font-family: monospace;
font-size: 0.9rem;
color: var(--text-primary, #1a1a1a);
word-break: break-all;
}
.webhook-events {
font-size: 0.8rem;
color: var(--text-secondary, #666);
margin-top: 0.25rem;
}
.audit-log-table {
width: 100%;
border-collapse: collapse;
margin-top: 1rem;
}
.audit-log-table th,
.audit-log-table td {
padding: 0.75rem;
text-align: left;
border-bottom: 1px solid var(--border-color, #e0e0e0);
}
.audit-log-table th {
font-weight: 500;
color: var(--text-secondary, #666);
font-size: 0.85rem;
text-transform: uppercase;
}
.audit-log-table td {
font-size: 0.95rem;
}
.audit-action {
display: inline-block;
padding: 0.25rem 0.5rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 4px;
font-size: 0.85rem;
}
.notification-settings {
display: grid;
gap: 0.75rem;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
border-radius: 8px;
padding: 2rem;
max-width: 500px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.modal-header h2 {
margin: 0;
font-size: 1.25rem;
}
.modal-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-secondary, #666);
}
</style>
</head>
<body>
<div class="org-settings-container">
<div class="settings-header">
<h1>Organization Settings</h1>
<div class="org-switcher" id="org-switcher">
<select class="form-input form-select" id="current-org" onchange="switchOrganization(this.value)">
<option value="org-1">My Organization</option>
<option value="org-2">Second Organization</option>
</select>
</div>
</div>
<div class="settings-tabs">
<button class="settings-tab active" data-tab="general">General</button>
<button class="settings-tab" data-tab="branding">Branding</button>
<button class="settings-tab" data-tab="members">Members</button>
<button class="settings-tab" data-tab="security">Security</button>
<button class="settings-tab" data-tab="billing">Billing & Usage</button>
<button class="settings-tab" data-tab="integrations">Integrations</button>
<button class="settings-tab" data-tab="advanced">Advanced</button>
</div>
<div id="general-panel" class="settings-panel active">
<div class="settings-section">
<h2>Organization Details</h2>
<form id="org-details-form" hx-post="/api/organizations/current" hx-swap="none">
<div class="form-group">
<label for="org-name">Organization Name</label>
<input type="text" id="org-name" name="name" class="form-input" value="My Organization" required>
</div>
<div class="form-group">
<label for="org-slug">Organization Slug</label>
<div class="form-input-group">
<span class="form-input" style="flex: 0; white-space: nowrap; background: var(--bg-secondary);">https://app.generalbots.com/</span>
<input type="text" id="org-slug" name="slug" class="form-input" value="my-organization" pattern="[a-z0-9-]+" required>
</div>
<p class="hint">Only lowercase letters, numbers, and hyphens allowed.</p>
</div>
<div class="form-group">
<label for="org-description">Description</label>
<textarea id="org-description" name="description" class="form-input form-textarea" placeholder="Describe your organization..."></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="org-industry">Industry</label>
<select id="org-industry" name="industry" class="form-input form-select">
<option value="">Select industry...</option>
<option value="technology">Technology</option>
<option value="healthcare">Healthcare</option>
<option value="finance">Finance</option>
<option value="education">Education</option>
<option value="retail">Retail</option>
<option value="manufacturing">Manufacturing</option>
<option value="other">Other</option>
</select>
</div>
<div class="form-group">
<label for="org-size">Organization Size</label>
<select id="org-size" name="size" class="form-input form-select">
<option value="1-10">1-10 employees</option>
<option value="11-50">11-50 employees</option>
<option value="51-200">51-200 employees</option>
<option value="201-1000">201-1000 employees</option>
<option value="1000+">1000+ employees</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="org-website">Website</label>
<input type="url" id="org-website" name="website" class="form-input" placeholder="https://example.com">
</div>
<div class="form-group">
<label for="org-timezone">Timezone</label>
<select id="org-timezone" name="timezone" class="form-input form-select">
<option value="UTC">UTC</option>
<option value="America/New_York">Eastern Time (US)</option>
<option value="America/Chicago">Central Time (US)</option>
<option value="America/Denver">Mountain Time (US)</option>
<option value="America/Los_Angeles">Pacific Time (US)</option>
<option value="Europe/London">London</option>
<option value="Europe/Paris">Paris</option>
<option value="Asia/Tokyo">Tokyo</option>
<option value="Asia/Shanghai">Shanghai</option>
<option value="America/Sao_Paulo">São Paulo</option>
</select>
</div>
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary">Save Changes</button>
<button type="reset" class="btn btn-secondary">Reset</button>
</div>
</form>
</div>
<div class="settings-section">
<h2>Contact Information</h2>
<form id="contact-form" hx-post="/api/organizations/current/contact" hx-swap="none">
<div class="form-row">
<div class="form-group">
<label for="contact-email">Contact Email</label>
<input type="email" id="contact-email" name="contact_email" class="form-input" placeholder="contact@example.com">
</div>
<div class="form-group">
<label for="contact-phone">Contact Phone</label>
<input type="tel" id="contact-phone" name="contact_phone" class="form-input" placeholder="+1 (555) 123-4567">
</div>
</div>
<div class="form-group">
<label for="billing-email">Billing Email</label>
<input type="email" id="billing-email" name="billing_email" class="form-input" placeholder="billing@example.com">
<p class="hint">Invoices and billing notifications will be sent to this address.</p>
</div>
<div class="form-group">
<label for="address">Address</label>
<textarea id="address" name="address" class="form-input form-textarea" rows="3" placeholder="Street address, City, State, ZIP, Country"></textarea>
</div>
<div class="btn-group">
<button type="submit" class="btn btn-primary">Save Contact Info</button>
</div>
</form>
</div>
</div>
<div id="branding-panel" class="settings-panel">
<div class="settings-section">
<h2>Brand Identity</h2>
<form id="branding-form" hx-post="/api/organizations/current/branding" hx-swap="none" hx-encoding="multipart/form-data">
<div class="form-group">
<label>Organization Logo</label>
<div class="logo-upload">
<div class="logo-preview" id="logo-preview">
<span class="logo-preview-placeholder">🏢</span>
</div>
<div class="logo-upload-actions">
<input type="file" id="logo-input" name="logo" accept="image/*" style="display: none;" onchange="previewLogo(this)">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('logo-input').click()">Upload Logo</button>
<button type="button" class="btn btn-secondary" onclick="removeLogo()">Remove</button>
<p class="hint">Recommended: 256x256px, PNG or SVG</p>
</div>
</div>
</div>
<div class="form-group">
<label>Favicon</label>
<div class="logo-upload">
<div class="logo-preview" id="favicon-preview" style="width: 48px; height: 48px;">
<span class="logo-preview-placeholder" style="font-size: 1.5rem;">🤖</span>
</div>
<div class="logo-upload-actions">
<input type="file" id="favicon-input" name="favicon" accept="image/*" style="display: none;" onchange="previewFavicon(this)">
<button type="button" class="btn btn-secondary" onclick="document.getElementById('favicon-input').click()">Upload Favicon</button>
<p class="hint">32x32px or 64x64px, ICO or PNG</p>
</div>
</div>
</div>
<h3>Brand Colors</h3>
<div class="form-row">
<div class="form-group">
<label for="primary-color">Primary Color</label>
<div class="color-picker-group">
<input type="color" id="primary-color" name="primary_color" class="color-picker" value="#0066cc" onchange="updateColorValue(this, 'primary-color-value')">
<span class="color-value" id="primary-color-value">#0066cc</span>
</div>
</div>
<div class="form-group">
<label for="secondary-color">Secondary Color</label>
<div class="color-picker-group">
<input type="color" id="secondary-color" name="secondary_color" class="color-picker" value="#6c757d" onchange="updateColorValue(this, 'secondary-color-value')">
<span class="color-value" id="secondary-color-value">#6c757d</span>
</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="accent-color">Accent Color</label>
<div class="color-picker-group">
<input type="color" id="accent-color" name="accent_color" class="color-picker" value="#28a745" onchange="updateColorValue(this, 'accent-color-value')">
<span class="color-value" id="accent-color-value">#28a745</span>
</div>
</div>
<div class="form-group">
<label for="background-color">Background Color</label>
<div class="color-picker-group">
<input type="color" id="background-color" name="background_color" class="color-picker" value="#ffffff" onchange="updateColorValue(this, 'background-color-value')">
<span class="color-value" id="background-color-value">#ffffff</span>
</div>
</div>
</div>
<h3>Custom Domain</h3>
<div class="form-group">
<label for="custom-domain">Custom Domain</label>
<input type="text" id="custom-domain" name="custom_domain" class="form-input" placeholder="chat.yourdomain.com">
<p class="hint">Point your

View file

@ -0,0 +1,822 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Organization Switcher - General Bots</title>
<link rel="stylesheet" href="../css/theme.css">
<link rel="stylesheet" href="admin.css">
<style>
.org-switcher-container {
position: relative;
display: inline-block;
}
.org-switcher-trigger {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: var(--surface-color, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
min-width: 200px;
}
.org-switcher-trigger:hover {
background: var(--surface-hover, #313244);
border-color: var(--primary-color, #89b4fa);
}
.org-switcher-trigger.active {
border-color: var(--primary-color, #89b4fa);
box-shadow: 0 0 0 2px rgba(137, 180, 250, 0.2);
}
.org-avatar {
width: 32px;
height: 32px;
border-radius: 6px;
background: linear-gradient(135deg, var(--primary-color, #89b4fa), var(--secondary-color, #cba6f7));
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
color: var(--text-on-primary, #1e1e2e);
flex-shrink: 0;
}
.org-avatar.personal {
background: linear-gradient(135deg, #a6e3a1, #94e2d5);
}
.org-info {
flex: 1;
text-align: left;
overflow: hidden;
}
.org-name {
font-weight: 500;
font-size: 14px;
color: var(--text-primary, #cdd6f4);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.org-plan {
font-size: 11px;
color: var(--text-secondary, #a6adc8);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.org-switcher-chevron {
width: 16px;
height: 16px;
color: var(--text-secondary, #a6adc8);
transition: transform 0.2s ease;
}
.org-switcher-trigger.active .org-switcher-chevron {
transform: rotate(180deg);
}
.org-switcher-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
min-width: 280px;
background: var(--surface-color, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
z-index: 1000;
opacity: 0;
visibility: hidden;
transform: translateY(-8px);
transition: all 0.2s ease;
}
.org-switcher-dropdown.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.org-dropdown-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border-color, #313244);
}
.org-dropdown-title {
font-size: 11px;
font-weight: 600;
color: var(--text-secondary, #a6adc8);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.org-search {
padding: 8px 16px;
border-bottom: 1px solid var(--border-color, #313244);
}
.org-search-input {
width: 100%;
padding: 8px 12px;
background: var(--input-bg, #11111b);
border: 1px solid var(--border-color, #313244);
border-radius: 6px;
color: var(--text-primary, #cdd6f4);
font-size: 13px;
outline: none;
transition: border-color 0.2s ease;
}
.org-search-input::placeholder {
color: var(--text-tertiary, #6c7086);
}
.org-search-input:focus {
border-color: var(--primary-color, #89b4fa);
}
.org-list {
max-height: 300px;
overflow-y: auto;
padding: 8px;
}
.org-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.15s ease;
}
.org-list-item:hover {
background: var(--surface-hover, #313244);
}
.org-list-item.selected {
background: rgba(137, 180, 250, 0.15);
}
.org-list-item.selected .org-name {
color: var(--primary-color, #89b4fa);
}
.org-item-avatar {
width: 36px;
height: 36px;
border-radius: 6px;
background: linear-gradient(135deg, var(--primary-color, #89b4fa), var(--secondary-color, #cba6f7));
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
color: var(--text-on-primary, #1e1e2e);
flex-shrink: 0;
}
.org-item-avatar.personal {
background: linear-gradient(135deg, #a6e3a1, #94e2d5);
}
.org-item-info {
flex: 1;
overflow: hidden;
}
.org-item-name {
font-weight: 500;
font-size: 14px;
color: var(--text-primary, #cdd6f4);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.org-item-meta {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--text-secondary, #a6adc8);
}
.org-item-role {
padding: 2px 6px;
background: var(--surface-hover, #313244);
border-radius: 4px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.org-item-role.owner {
background: rgba(249, 226, 175, 0.2);
color: #f9e2af;
}
.org-item-role.admin {
background: rgba(137, 180, 250, 0.2);
color: #89b4fa;
}
.org-item-check {
width: 20px;
height: 20px;
color: var(--primary-color, #89b4fa);
opacity: 0;
transition: opacity 0.15s ease;
}
.org-list-item.selected .org-item-check {
opacity: 1;
}
.org-dropdown-footer {
padding: 8px;
border-top: 1px solid var(--border-color, #313244);
}
.org-action-btn {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
padding: 10px 12px;
background: transparent;
border: none;
border-radius: 8px;
color: var(--text-primary, #cdd6f4);
font-size: 13px;
cursor: pointer;
transition: background 0.15s ease;
text-decoration: none;
}
.org-action-btn:hover {
background: var(--surface-hover, #313244);
}
.org-action-btn svg {
width: 18px;
height: 18px;
color: var(--text-secondary, #a6adc8);
}
.org-action-btn.create {
color: var(--primary-color, #89b4fa);
}
.org-action-btn.create svg {
color: var(--primary-color, #89b4fa);
}
.org-divider {
height: 1px;
background: var(--border-color, #313244);
margin: 4px 0;
}
.org-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.org-loading-spinner {
width: 24px;
height: 24px;
border: 2px solid var(--border-color, #313244);
border-top-color: var(--primary-color, #89b4fa);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.org-empty {
text-align: center;
padding: 24px;
color: var(--text-secondary, #a6adc8);
}
.org-empty-icon {
font-size: 32px;
margin-bottom: 8px;
}
.notification-badge {
position: absolute;
top: -4px;
right: -4px;
min-width: 18px;
height: 18px;
padding: 0 5px;
background: #f38ba8;
border-radius: 9px;
font-size: 11px;
font-weight: 600;
color: #1e1e2e;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.org-switcher-trigger {
min-width: auto;
padding: 8px;
}
.org-info {
display: none;
}
.org-switcher-dropdown {
position: fixed;
left: 16px;
right: 16px;
top: auto;
bottom: 16px;
border-radius: 16px;
}
}
</style>
</head>
<body>
<div id="org-switcher" class="org-switcher-container">
<button class="org-switcher-trigger" id="org-trigger" aria-haspopup="true" aria-expanded="false">
<div class="org-avatar" id="current-org-avatar">A</div>
<div class="org-info">
<div class="org-name" id="current-org-name">Acme Corporation</div>
<div class="org-plan" id="current-org-plan">Pro Plan</div>
</div>
<svg class="org-switcher-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6,9 12,15 18,9"></polyline>
</svg>
</button>
<div class="org-switcher-dropdown" id="org-dropdown">
<div class="org-dropdown-header">
<div class="org-dropdown-title">Switch Organization</div>
</div>
<div class="org-search">
<input type="text" class="org-search-input" id="org-search" placeholder="Search organizations...">
</div>
<div class="org-list" id="org-list">
<div class="org-loading" id="org-loading">
<div class="org-loading-spinner"></div>
</div>
</div>
<div class="org-dropdown-footer">
<a href="organization-settings.html" class="org-action-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
Organization Settings
</a>
<div class="org-divider"></div>
<button class="org-action-btn create" id="create-org-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Create New Organization
</button>
</div>
</div>
</div>
<div id="create-org-modal" class="modal" style="display: none;">
<div class="modal-overlay"></div>
<div class="modal-content">
<div class="modal-header">
<h2>Create New Organization</h2>
<button class="modal-close" id="close-modal">&times;</button>
</div>
<form id="create-org-form">
<div class="form-group">
<label for="new-org-name">Organization Name</label>
<input type="text" id="new-org-name" required placeholder="Enter organization name">
</div>
<div class="form-group">
<label for="new-org-slug">URL Slug</label>
<input type="text" id="new-org-slug" required placeholder="organization-slug">
<small>This will be used in URLs: app.generalbots.ai/<span id="slug-preview">organization-slug</span></small>
</div>
<div class="form-group">
<label for="new-org-plan">Plan</label>
<select id="new-org-plan">
<option value="free">Free</option>
<option value="starter">Starter</option>
<option value="pro" selected>Pro</option>
<option value="enterprise">Enterprise</option>
</select>
</div>
<div class="form-actions">
<button type="button" class="btn-secondary" id="cancel-create">Cancel</button>
<button type="submit" class="btn-primary">Create Organization</button>
</div>
</form>
</div>
</div>
<script>
const OrganizationSwitcher = {
currentOrg: null,
organizations: [],
isOpen: false,
init() {
this.bindEvents();
this.loadOrganizations();
},
bindEvents() {
const trigger = document.getElementById('org-trigger');
const dropdown = document.getElementById('org-dropdown');
const searchInput = document.getElementById('org-search');
const createBtn = document.getElementById('create-org-btn');
const modal = document.getElementById('create-org-modal');
const closeModal = document.getElementById('close-modal');
const cancelCreate = document.getElementById('cancel-create');
const createForm = document.getElementById('create-org-form');
const newOrgName = document.getElementById('new-org-name');
const newOrgSlug = document.getElementById('new-org-slug');
const slugPreview = document.getElementById('slug-preview');
trigger.addEventListener('click', () => this.toggleDropdown());
document.addEventListener('click', (e) => {
if (!e.target.closest('#org-switcher') && this.isOpen) {
this.closeDropdown();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isOpen) {
this.closeDropdown();
}
});
searchInput.addEventListener('input', (e) => this.filterOrganizations(e.target.value));
createBtn.addEventListener('click', () => this.openCreateModal());
closeModal.addEventListener('click', () => this.closeCreateModal());
cancelCreate.addEventListener('click', () => this.closeCreateModal());
modal.querySelector('.modal-overlay').addEventListener('click', () => this.closeCreateModal());
newOrgName.addEventListener('input', (e) => {
const slug = this.generateSlug(e.target.value);
newOrgSlug.value = slug;
slugPreview.textContent = slug || 'organization-slug';
});
newOrgSlug.addEventListener('input', (e) => {
slugPreview.textContent = e.target.value || 'organization-slug';
});
createForm.addEventListener('submit', (e) => this.handleCreateOrg(e));
},
async loadOrganizations() {
const loading = document.getElementById('org-loading');
const list = document.getElementById('org-list');
try {
const response = await fetch('/api/organizations');
if (response.ok) {
this.organizations = await response.json();
} else {
this.organizations = this.getMockOrganizations();
}
} catch {
this.organizations = this.getMockOrganizations();
}
loading.style.display = 'none';
this.renderOrganizations(this.organizations);
const storedOrgId = localStorage.getItem('currentOrganizationId');
const currentOrg = this.organizations.find(o => o.id === storedOrgId) || this.organizations[0];
if (currentOrg) {
this.selectOrganization(currentOrg, false);
}
},
getMockOrganizations() {
return [
{
id: 'personal',
name: 'Personal',
slug: 'personal',
plan: 'Free',
role: 'owner',
avatar: null,
isPersonal: true,
memberCount: 1
},
{
id: 'org-1',
name: 'Acme Corporation',
slug: 'acme',
plan: 'Pro',
role: 'owner',
avatar: 'A',
isPersonal: false,
memberCount: 15
},
{
id: 'org-2',
name: 'Tech Startup Inc',
slug: 'techstartup',
plan: 'Starter',
role: 'admin',
avatar: 'T',
isPersonal: false,
memberCount: 8
},
{
id: 'org-3',
name: 'Enterprise Solutions',
slug: 'enterprise-solutions',
plan: 'Enterprise',
role: 'member',
avatar: 'E',
isPersonal: false,
memberCount: 250
}
];
},
renderOrganizations(orgs) {
const list = document.getElementById('org-list');
if (orgs.length === 0) {
list.innerHTML = `
<div class="org-empty">
<div class="org-empty-icon">🏢</div>
<p>No organizations found</p>
</div>
`;
return;
}
list.innerHTML = orgs.map(org => `
<div class="org-list-item ${this.currentOrg?.id === org.id ? 'selected' : ''}"
data-org-id="${org.id}"
onclick="OrganizationSwitcher.selectOrganization(${JSON.stringify(org).replace(/"/g, '&quot;')})">
<div class="org-item-avatar ${org.isPersonal ? 'personal' : ''}">
${org.isPersonal ? '👤' : (org.avatar || org.name.charAt(0).toUpperCase())}
</div>
<div class="org-item-info">
<div class="org-item-name">${this.escapeHtml(org.name)}</div>
<div class="org-item-meta">
<span class="org-item-role ${org.role}">${org.role}</span>
<span>${org.memberCount} member${org.memberCount !== 1 ? 's' : ''}</span>
</div>
</div>
<svg class="org-item-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20,6 9,17 4,12"></polyline>
</svg>
</div>
`).join('');
},
filterOrganizations(query) {
const filtered = this.organizations.filter(org =>
org.name.toLowerCase().includes(query.toLowerCase()) ||
org.slug.toLowerCase().includes(query.toLowerCase())
);
this.renderOrganizations(filtered);
},
selectOrganization(org, closeAfter = true) {
this.currentOrg = org;
localStorage.setItem('currentOrganizationId', org.id);
document.getElementById('current-org-avatar').textContent =
org.isPersonal ? '👤' : (org.avatar || org.name.charAt(0).toUpperCase());
document.getElementById('current-org-avatar').className =
'org-avatar' + (org.isPersonal ? ' personal' : '');
document.getElementById('current-org-name').textContent = org.name;
document.getElementById('current-org-plan').textContent = org.plan + ' Plan';
document.querySelectorAll('.org-list-item').forEach(item => {
item.classList.toggle('selected', item.dataset.orgId === org.id);
});
if (closeAfter) {
this.closeDropdown();
}
window.dispatchEvent(new CustomEvent('organizationChanged', {
detail: { organization: org }
}));
},
toggleDropdown() {
if (this.isOpen) {
this.closeDropdown();
} else {
this.openDropdown();
}
},
openDropdown() {
const trigger = document.getElementById('org-trigger');
const dropdown = document.getElementById('org-dropdown');
trigger.classList.add('active');
trigger.setAttribute('aria-expanded', 'true');
dropdown.classList.add('show');
this.isOpen = true;
document.getElementById('org-search').value = '';
this.renderOrganizations(this.organizations);
document.getElementById('org-search').focus();
},
closeDropdown() {
const trigger = document.getElementById('org-trigger');
const dropdown = document.getElementById('org-dropdown');
trigger.classList.remove('active');
trigger.setAttribute('aria-expanded', 'false');
dropdown.classList.remove('show');
this.isOpen = false;
},
openCreateModal() {
this.closeDropdown();
document.getElementById('create-org-modal').style.display = 'flex';
document.getElementById('new-org-name').focus();
},
closeCreateModal() {
document.getElementById('create-org-modal').style.display = 'none';
document.getElementById('create-org-form').reset();
document.getElementById('slug-preview').textContent = 'organization-slug';
},
generateSlug(name) {
return name
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.trim();
},
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
},
async handleCreateOrg(e) {
e.preventDefault();
const name = document.getElementById('new-org-name').value;
const slug = document.getElementById('new-org-slug').value;
const plan = document.getElementById('new-org-plan').value;
const newOrg = {
id: 'org-' + Date.now(),
name,
slug,
plan: plan.charAt(0).toUpperCase() + plan.slice(1),
role: 'owner',
avatar: name.charAt(0).toUpperCase(),
isPersonal: false,
memberCount: 1
};
try {
const response = await fetch('/api/organizations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, slug, plan })
});
if (response.ok) {
const created = await response.json();
this.organizations.push(created);
this.selectOrganization(created);
} else {
this.organizations.push(newOrg);
this.selectOrganization(newOrg);
}
} catch {
this.organizations.push(newOrg);
this.selectOrganization(newOrg);
}
this.closeCreateModal();
}
};
document.addEventListener('DOMContentLoaded', () => OrganizationSwitcher.init());
window.OrganizationSwitcher = OrganizationSwitcher;
</script>
<style>
.modal {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
.modal-content {
position: relative;
background: var(--surface-color, #1e1e2e);
border: 1px solid var(--border-color, #313244);
border-radius: 16px;
padding: 24px;
width: 90%;
max-width: 480px;
box-shadow: 0 16px 64px rgba(0, 0, 0, 0.5);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.modal-header h2 {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--text-primary, #cdd6f4);
}
.modal-close {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary, #a6adc8);
font-size: 24px;
cursor: pointer;
transition: background 0.15s ease;
}
.modal-close:hover {
background: var(--surface-hover, #313244);
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 13px;
font-weight: 500;
color: var(--text-primary, #cdd6f4);
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px 14px;

655
ui/suite/admin/roles.html Normal file
View file

@ -0,0 +1,655 @@
<div class="roles-view">
<div class="page-header">
<div class="header-left">
<h1>Role Management</h1>
<p class="subtitle">Configure roles and assign permissions to control access across the suite</p>
</div>
<div class="header-actions">
<button class="btn-secondary" onclick="document.getElementById('import-roles-modal').showModal()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
Import
</button>
<button class="btn-primary" onclick="document.getElementById('create-role-modal').showModal()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
<line x1="12" y1="8" x2="12" y2="14"></line>
<line x1="9" y1="11" x2="15" y2="11"></line>
</svg>
Create Role
</button>
</div>
</div>
<div class="roles-layout">
<div class="roles-sidebar">
<div class="sidebar-header">
<h3>Roles</h3>
<div class="role-filters">
<select id="role-type-filter" onchange="filterRoles(this.value)">
<option value="all">All Roles</option>
<option value="system">System Roles</option>
<option value="custom">Custom Roles</option>
</select>
</div>
</div>
<div class="search-box">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text"
placeholder="Search roles..."
name="q"
hx-get="/api/rbac/roles/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#roles-list"
hx-swap="innerHTML">
</div>
<div class="roles-list" id="roles-list"
hx-get="/api/rbac/roles"
hx-trigger="load"
hx-swap="innerHTML">
<div class="loading-state">
<div class="spinner"></div>
</div>
</div>
</div>
<div class="roles-content">
<div class="role-detail-placeholder" id="role-placeholder">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
<h3>Select a Role</h3>
<p>Choose a role from the list to view and manage its permissions</p>
</div>
<div class="role-detail-content" id="role-detail" style="display: none;">
<div class="role-header">
<div class="role-header-info">
<div class="role-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
</div>
<div class="role-title-section">
<h2 id="role-display-name">Role Name</h2>
<span class="role-name-badge" id="role-name">role_name</span>
<span class="role-type-badge system" id="role-type-badge">System</span>
</div>
</div>
<div class="role-header-actions">
<button class="btn-secondary" id="btn-duplicate-role" onclick="duplicateRole()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
Duplicate
</button>
<button class="btn-danger" id="btn-delete-role" onclick="confirmDeleteRole()" disabled>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
Delete
</button>
</div>
</div>
<div class="role-description" id="role-description">
<p>Role description goes here</p>
</div>
<div class="role-stats">
<div class="stat-card">
<div class="stat-value" id="stat-users">0</div>
<div class="stat-label">Users</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-groups">0</div>
<div class="stat-label">Groups</div>
</div>
<div class="stat-card">
<div class="stat-value" id="stat-permissions">0</div>
<div class="stat-label">Permissions</div>
</div>
</div>
<div class="role-tabs">
<button class="tab-btn active" onclick="showTab('permissions', this)">Permissions</button>
<button class="tab-btn" onclick="showTab('users', this)">Assigned Users</button>
<button class="tab-btn" onclick="showTab('groups', this)">Assigned Groups</button>
<button class="tab-btn" onclick="showTab('audit', this)">Audit Log</button>
</div>
<div class="tab-content" id="tab-permissions">
<div class="permissions-assignment">
<div class="assignment-header">
<h4>Permission Assignment</h4>
<p class="help-text">Use the dual-list to assign permissions to this role</p>
</div>
<div class="dual-list-container">
<div class="list-box available-box">
<div class="list-box-header">
<span>Available Permissions</span>
<span class="count" id="available-count">0</span>
</div>
<div class="list-box-search">
<input type="text" placeholder="Filter available..." onkeyup="filterAvailable(this.value)">
</div>
<div class="category-filter">
<select onchange="filterByCategory(this.value, 'available')">
<option value="">All Categories</option>
<option value="admin">Administration</option>
<option value="compliance">Compliance</option>
<option value="security">Security</option>
<option value="mail">Mail</option>
<option value="calendar">Calendar</option>
<option value="drive">Drive</option>
<option value="docs">Documents</option>
<option value="sheet">Spreadsheets</option>
<option value="slides">Presentations</option>
<option value="meet">Meetings</option>
<option value="chat">Chat</option>
<option value="tasks">Tasks</option>
<option value="ai">AI & Bots</option>
<option value="analytics">Analytics</option>
<option value="integrations">Integrations</option>
<option value="automation">Automation</option>
</select>
</div>
<div class="list-box-content" id="available-permissions">
<div class="loading-state"><div class="spinner"></div></div>
</div>
</div>
<div class="transfer-buttons">
<button class="transfer-btn" onclick="assignSelected()" title="Assign selected permissions">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
<button class="transfer-btn" onclick="assignAll()" title="Assign all permissions">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="13 17 18 12 13 7"></polyline>
<polyline points="6 17 11 12 6 7"></polyline>
</svg>
</button>
<button class="transfer-btn" onclick="removeSelected()" title="Remove selected permissions">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<button class="transfer-btn" onclick="removeAll()" title="Remove all permissions">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="18 17 13 12 18 7"></polyline>
<polyline points="11 17 6 12 11 7"></polyline>
</svg>
</button>
</div>
<div class="list-box assigned-box">
<div class="list-box-header">
<span>Assigned Permissions</span>
<span class="count" id="assigned-count">0</span>
</div>
<div class="list-box-search">
<input type="text" placeholder="Filter assigned..." onkeyup="filterAssigned(this.value)">
</div>
<div class="category-filter">
<select onchange="filterByCategory(this.value, 'assigned')">
<option value="">All Categories</option>
<option value="admin">Administration</option>
<option value="compliance">Compliance</option>
<option value="security">Security</option>
<option value="mail">Mail</option>
<option value="calendar">Calendar</option>
<option value="drive">Drive</option>
<option value="docs">Documents</option>
<option value="sheet">Spreadsheets</option>
<option value="slides">Presentations</option>
<option value="meet">Meetings</option>
<option value="chat">Chat</option>
<option value="tasks">Tasks</option>
<option value="ai">AI & Bots</option>
<option value="analytics">Analytics</option>
<option value="integrations">Integrations</option>
<option value="automation">Automation</option>
</select>
</div>
<div class="list-box-content" id="assigned-permissions">
<div class="empty-state">No permissions assigned</div>
</div>
</div>
</div>
<div class="assignment-footer">
<button class="btn-secondary" onclick="resetPermissions()">Reset to Default</button>
<button class="btn-primary" onclick="savePermissions()" id="btn-save-permissions">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
<polyline points="7 3 7 8 15 8"></polyline>
</svg>
Save Changes
</button>
</div>
</div>
</div>
<div class="tab-content" id="tab-users" style="display: none;">
<div class="users-assignment">
<div class="assignment-header">
<h4>Users with this Role</h4>
<button class="btn-secondary btn-sm" onclick="document.getElementById('add-user-modal').showModal()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add User
</button>
</div>
<div class="assigned-users-list" id="assigned-users">
<div class="loading-state"><div class="spinner"></div></div>
</div>
</div>
</div>
<div class="tab-content" id="tab-groups" style="display: none;">
<div class="groups-assignment">
<div class="assignment-header">
<h4>Groups with this Role</h4>
<button class="btn-secondary btn-sm" onclick="document.getElementById('add-group-modal').showModal()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
Add Group
</button>
</div>
<div class="assigned-groups-list" id="assigned-groups">
<div class="loading-state"><div class="spinner"></div></div>
</div>
</div>
</div>
<div class="tab-content" id="tab-audit" style="display: none;">
<div class="audit-log">
<div class="assignment-header">
<h4>Role Change History</h4>
<button class="btn-secondary btn-sm" onclick="exportAuditLog()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Export
</button>
</div>
<div class="audit-log-list" id="audit-log">
<div class="loading-state"><div class="spinner"></div></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<dialog id="create-role-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Create New Role</h2>
<button class="close-btn" onclick="this.closest('dialog').close()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<form hx-post="/api/rbac/roles" hx-target="#roles-list" hx-swap="innerHTML" hx-on::after-request="this.closest('dialog').close(); this.reset();">
<div class="form-group">
<label>Role Name <span class="required">*</span></label>
<input type="text" name="name" placeholder="e.g., content_editor" required
pattern="[a-z_]+" title="Lowercase letters and underscores only">
<p class="help-text">Lowercase, no spaces (use underscores)</p>
</div>
<div class="form-group">
<label>Display Name <span class="required">*</span></label>
<input type="text" name="display_name" placeholder="e.g., Content Editor" required>
</div>
<div class="form-group">
<label>Description</label>
<textarea name="description" rows="3" placeholder="Describe what this role is for..."></textarea>
</div>
<div class="form-group">
<label>Copy Permissions From</label>
<select name="copy_from">
<option value="">Start with no permissions</option>
<option value="standard_user">Standard User</option>
<option value="power_user">Power User</option>
<option value="viewer">Viewer</option>
<option value="guest_user">Guest User</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn-secondary" onclick="this.closest('dialog').close()">Cancel</button>
<button type="submit" class="btn-primary">Create Role</button>
</div>
</form>
</div>
</dialog>
<dialog id="delete-role-modal" class="modal modal-small">
<div class="modal-content">
<div class="modal-header">
<h2>Delete Role</h2>
<button class="close-btn" onclick="this.closest('dialog').close()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="warning-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</div>
<p class="warning-text">Are you sure you want to delete the role <strong id="delete-role-name">Role Name</strong>?</p>
<p class="warning-subtext">This will remove the role from all users and groups. This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn-danger" id="confirm-delete-btn" onclick="deleteRole()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
Delete Role
</button>
</div>
</div>
</dialog>
<dialog id="add-user-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Assign Role to Users</h2>
<button class="close-btn" onclick="this.closest('dialog').close()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Search Users</label>
<input type="text" id="user-search" placeholder="Type to search users..."
hx-get="/api/rbac/users/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#user-search-results"
hx-swap="innerHTML">
</div>
<div class="search-results" id="user-search-results">
<p class="empty-text">Type to search for users</p>
</div>
<div class="selected-items" id="selected-users">
</div>
<div class="form-group">
<label>Expiration (Optional)</label>
<input type="datetime-local" id="role-expires-at" name="expires_at">
<p class="help-text">Leave empty for permanent assignment</p>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn-primary" onclick="assignUsersToRole()">Assign Role</button>
</div>
</div>
</dialog>
<dialog id="add-group-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>Assign Role to Groups</h2>
<button class="close-btn" onclick="this.closest('dialog').close()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Search Groups</label>
<input type="text" id="group-search" placeholder="Type to search groups..."
hx-get="/api/rbac/groups/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#group-search-results"
hx-swap="innerHTML">
</div>
<div class="search-results" id="group-search-results">
<p class="empty-text">Type to search for groups</p>
</div>
<div class="selected-items" id="selected-groups">
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="this.closest('dialog').close()">Cancel</button>
<button class="btn-primary" onclick="assignGroupsToRole()">Assign Role</button>
</div>
</div>
</dialog>
<style>
.roles-view {
height: 100%;
display: flex;
flex-direction: column;
background: var(--bg-primary, #f5f5f5);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: white;
border-bottom: 1px solid #e0e0e0;
}
.header-left h1 {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
color: #1a1a1a;
}
.header-left .subtitle {
color: #666;
margin-top: 0.25rem;
font-size: 0.9rem;
}
.header-actions {
display: flex;
gap: 0.75rem;
}
.roles-layout {
display: flex;
flex: 1;
overflow: hidden;
}
.roles-sidebar {
width: 320px;
background: white;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid #e0e0e0;
}
.sidebar-header h3 {
margin: 0 0 0.75rem 0;
font-size: 1rem;
font-weight: 600;
}
.role-filters select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 0.875rem;
}
.search-box {
padding: 0.75rem 1rem;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.search-box svg {
color: #999;
flex-shrink: 0;
}
.search-box input {
flex: 1;
border: none;
outline: none;
font-size: 0.875rem;
}
.roles-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.role-item {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
margin-bottom: 0.25rem;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.role-item:hover {
background: #f5f5f5;
}
.role-item.selected {
background: #e3f2fd;
border-left: 3px solid #1976d2;
}
.role-item-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 0.75rem;
background: #e8f4fd;
color: #1976d2;
}
.role-item.system .role-item-icon {
background: #e8f5e9;
color: #2e7d32;
}
.role-item-info {
flex: 1;
min-width: 0;
}
.role-item-name {
font-weight: 500;
font-size: 0.9rem;
color: #1a1a1a;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.role-item-meta {
font-size: 0.75rem;
color: #666;
display: flex;
gap: 0.5rem;
margin-top: 0.125rem;
}
.roles-content {
flex: 1;
overflow-y: auto;
padding: 1.5rem 2rem;
}
.role-detail-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
text-align: center;
}
.role-detail-placeholder svg {
margin-bottom: 1rem;
opacity: 0.5;
}
.role-detail-placeholder h3 {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
color: #666;
}
.role-detail-content {
max-width: 1200px;
}
.role-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.role-header-info {
display: flex;
align-items: flex-start;
gap: 1rem;
}
.role-icon {
width: 56px;
height: 56px;
border-radius:

File diff suppressed because it is too large Load diff

View file

@ -289,6 +289,52 @@
<span>Analytics</span> <span>Analytics</span>
</a> </a>
<a
href="#video"
class="app-item"
role="menuitem"
hx-get="/video/video.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#f43f5e,
#e11d48
);
"
>
🎬
</div>
<span>Video</span>
</a>
<a
href="#learn"
class="app-item"
role="menuitem"
hx-get="/learn/learn.html"
hx-target="#main-content"
hx-push-url="true"
>
<div
class="app-item-icon"
style="
background: linear-gradient(
135deg,
#8b5cf6,
#7c3aed
);
"
>
📖
</div>
<span>Learn</span>
</a>
<a <a
href="#monitoring" href="#monitoring"
class="app-item" class="app-item"

View file

@ -0,0 +1,933 @@
.dashboards-container {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-primary, #0a0a0a);
color: var(--color-text-primary, #e5e5e5);
}
.dashboards-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid var(--color-border, #2a2a2a);
background: var(--color-bg-secondary, #111);
}
.dashboards-header .header-title {
display: flex;
align-items: center;
gap: 12px;
}
.dashboards-header .header-title h2 {
font-size: 20px;
font-weight: 600;
margin: 0;
}
.dashboards-header .header-icon {
color: var(--color-accent, #d4f505);
}
.dashboards-header .header-actions {
display: flex;
align-items: center;
gap: 12px;
}
.dashboards-header .search-box {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--color-bg-tertiary, #1a1a1a);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 8px;
}
.dashboards-header .search-box input {
background: transparent;
border: none;
color: inherit;
outline: none;
width: 200px;
font-size: 14px;
}
.dashboards-header .search-box input::placeholder {
color: var(--color-text-tertiary, #666);
}
.dashboards-header .filter-select {
padding: 8px 12px;
background: var(--color-bg-tertiary, #1a1a1a);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 8px;
color: inherit;
font-size: 14px;
cursor: pointer;
}
.dashboards-header .btn-primary {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: var(--color-accent, #d4f505);
color: #000;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.dashboards-header .btn-primary:hover {
opacity: 0.9;
}
.dashboards-layout {
display: flex;
flex: 1;
overflow: hidden;
}
.dashboards-sidebar {
width: 260px;
padding: 16px;
border-right: 1px solid var(--color-border, #2a2a2a);
background: var(--color-bg-secondary, #111);
overflow-y: auto;
flex-shrink: 0;
}
.sidebar-section {
margin-bottom: 24px;
}
.sidebar-section h3 {
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary, #999);
margin-bottom: 12px;
}
.dashboard-nav {
display: flex;
flex-direction: column;
gap: 4px;
}
.dashboard-nav a,
.dashboard-nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border-radius: 8px;
color: var(--color-text-primary, #e5e5e5);
text-decoration: none;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
.dashboard-nav a:hover,
.dashboard-nav-item:hover {
background: var(--color-bg-tertiary, #1a1a1a);
}
.dashboard-nav a.active,
.dashboard-nav-item.active {
background: var(--color-accent-dim, rgba(212, 245, 5, 0.1));
color: var(--color-accent, #d4f505);
}
.data-sources-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.data-source-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 10px;
background: var(--color-bg-tertiary, #1a1a1a);
border-radius: 6px;
font-size: 13px;
}
.data-source-item .status {
width: 8px;
height: 8px;
border-radius: 50%;
}
.data-source-item .status.active {
background: #10b981;
}
.data-source-item .status.error {
background: #ef4444;
}
.data-source-item .status.inactive {
background: #6b7280;
}
.btn-link {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 0;
background: none;
border: none;
color: var(--color-accent, #d4f505);
font-size: 13px;
cursor: pointer;
}
.btn-link:hover {
text-decoration: underline;
}
.dashboards-main {
flex: 1;
padding: 24px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 24px;
}
.dashboards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
}
.dashboard-card {
background: var(--color-bg-secondary, #111);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition:
transform 0.2s,
box-shadow 0.2s;
}
.dashboard-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
border-color: var(--color-accent, #d4f505);
}
.dashboard-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 16px;
}
.dashboard-card-header h4 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
}
.dashboard-card-header p {
margin: 0;
font-size: 13px;
color: var(--color-text-secondary, #999);
}
.dashboard-card-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.dashboard-card:hover .dashboard-card-actions {
opacity: 1;
}
.dashboard-card-actions button {
padding: 6px;
background: var(--color-bg-tertiary, #1a1a1a);
border: none;
border-radius: 6px;
color: var(--color-text-secondary, #999);
cursor: pointer;
}
.dashboard-card-actions button:hover {
color: var(--color-text-primary, #e5e5e5);
}
.dashboard-card-preview {
height: 160px;
padding: 16px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-bg-tertiary, #1a1a1a);
}
.dashboard-card-preview .preview-widgets {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
width: 100%;
height: 100%;
}
.preview-widget {
background: var(--color-bg-secondary, #111);
border-radius: 6px;
}
.dashboard-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-top: 1px solid var(--color-border, #2a2a2a);
}
.dashboard-card-tags {
display: flex;
gap: 6px;
}
.tag {
padding: 4px 8px;
background: var(--color-bg-tertiary, #1a1a1a);
border-radius: 4px;
font-size: 11px;
color: var(--color-text-secondary, #999);
}
.dashboard-card-meta {
font-size: 12px;
color: var(--color-text-tertiary, #666);
}
.dashboard-viewer {
flex: 1;
display: flex;
flex-direction: column;
background: var(--color-bg-primary, #0a0a0a);
}
.viewer-toolbar {
display: flex;
align-items: center;
gap: 16px;
padding: 12px 20px;
background: var(--color-bg-secondary, #111);
border-bottom: 1px solid var(--color-border, #2a2a2a);
}
.viewer-toolbar h3 {
flex: 1;
margin: 0;
font-size: 18px;
font-weight: 600;
}
.viewer-toolbar .btn-icon {
padding: 8px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--color-text-secondary, #999);
cursor: pointer;
transition: all 0.2s;
}
.viewer-toolbar .btn-icon:hover {
background: var(--color-bg-tertiary, #1a1a1a);
color: var(--color-text-primary, #e5e5e5);
}
.viewer-toolbar .back-btn {
margin-right: 8px;
}
.viewer-actions {
display: flex;
align-items: center;
gap: 8px;
}
.date-range-select {
padding: 8px 12px;
background: var(--color-bg-tertiary, #1a1a1a);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 6px;
color: inherit;
font-size: 13px;
cursor: pointer;
}
.viewer-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.viewer-content.edit-mode {
background: repeating-linear-gradient(
0deg,
transparent,
transparent 40px,
var(--color-border, #2a2a2a) 40px,
var(--color-border, #2a2a2a) 41px
),
repeating-linear-gradient(
90deg,
transparent,
transparent 40px,
var(--color-border, #2a2a2a) 40px,
var(--color-border, #2a2a2a) 41px
);
}
.widget-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 16px;
min-height: 100%;
}
.widget {
background: var(--color-bg-secondary, #111);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 12px;
overflow: hidden;
}
.widget-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--color-border, #2a2a2a);
}
.widget-header h5 {
margin: 0;
font-size: 14px;
font-weight: 500;
}
.widget-content {
padding: 16px;
height: calc(100% - 48px);
}
.conversational-query {
background: var(--color-bg-secondary, #111);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 12px;
padding: 16px;
}
.query-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 12px;
color: var(--color-text-secondary, #999);
font-size: 14px;
}
.query-input-wrapper {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.query-input-wrapper input {
flex: 1;
padding: 12px 16px;
background: var(--color-bg-tertiary, #1a1a1a);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 8px;
color: inherit;
font-size: 14px;
}
.query-input-wrapper input:focus {
outline: none;
border-color: var(--color-accent, #d4f505);
}
.query-submit-btn {
padding: 12px 16px;
background: var(--color-accent, #d4f505);
border: none;
border-radius: 8px;
color: #000;
cursor: pointer;
transition: opacity 0.2s;
}
.query-submit-btn:hover {
opacity: 0.9;
}
.query-source-select {
padding: 8px 12px;
background: var(--color-bg-tertiary, #1a1a1a);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 6px;
color: inherit;
font-size: 13px;
margin-bottom: 12px;
}
.query-loading {
display: flex;
align-items: center;
gap: 10px;
padding: 12px;
color: var(--color-text-secondary, #999);
font-size: 14px;
}
.query-results {
margin-top: 16px;
}
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-backdrop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
}
.modal-content {
position: relative;
width: 90%;
max-width: 540px;
max-height: 90vh;
overflow-y: auto;
background: var(--color-bg-secondary, #111);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 16px;
}
.modal-content.modal-lg {
max-width: 720px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--color-border, #2a2a2a);
}
.modal-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.modal-close {
padding: 8px;
background: transparent;
border: none;
color: var(--color-text-secondary, #999);
cursor: pointer;
border-radius: 6px;
}
.modal-close:hover {
background: var(--color-bg-tertiary, #1a1a1a);
color: var(--color-text-primary, #e5e5e5);
}
.modal-content form {
padding: 24px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: var(--color-text-secondary, #999);
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 10px 14px;
background: var(--color-bg-tertiary, #1a1a1a);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 8px;
color: inherit;
font-size: 14px;
font-family: inherit;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
outline: none;
border-color: var(--color-accent, #d4f505);
}
.form-row {
display: flex;
gap: 16px;
}
.form-row .form-group {
flex: 1;
}
.checkbox-group label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--color-accent, #d4f505);
}
.template-selection .template-options {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.template-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 20px;
background: var(--color-bg-tertiary, #1a1a1a);
border: 2px solid var(--color-border, #2a2a2a);
border-radius: 12px;
color: var(--color-text-secondary, #999);
cursor: pointer;
transition: all 0.2s;
}
.template-option:hover {
border-color: var(--color-text-tertiary, #666);
color: var(--color-text-primary, #e5e5e5);
}
.template-option.active {
border-color: var(--color-accent, #d4f505);
color: var(--color-accent, #d4f505);
background: rgba(212, 245, 5, 0.05);
}
.template-option span {
font-size: 12px;
font-weight: 500;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 24px;
border-top: 1px solid var(--color-border, #2a2a2a);
}
.btn-secondary {
padding: 10px 20px;
background: var(--color-bg-tertiary, #1a1a1a);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 8px;
color: var(--color-text-primary, #e5e5e5);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary:hover {
background: var(--color-bg-secondary, #111);
border-color: var(--color-text-tertiary, #666);
}
.modal-footer .btn-primary {
padding: 10px 20px;
background: var(--color-accent, #d4f505);
border: none;
border-radius: 8px;
color: #000;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: opacity 0.2s;
}
.modal-footer .btn-primary:hover {
opacity: 0.9;
}
.widget-type-grid {
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
.widget-category h4 {
margin: 0 0 12px 0;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-secondary, #999);
}
.widget-options {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.widget-option {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 16px 20px;
min-width: 100px;
background: var(--color-bg-tertiary, #1a1a1a);
border: 2px solid var(--color-border, #2a2a2a);
border-radius: 12px;
color: var(--color-text-secondary, #999);
cursor: pointer;
transition: all 0.2s;
}
.widget-option:hover {
border-color: var(--color-accent, #d4f505);
color: var(--color-accent, #d4f505);
background: rgba(212, 245, 5, 0.05);
}
.widget-option span {
font-size: 12px;
font-weight: 500;
}
.loading-placeholder {
display: flex;
flex-direction: column;
gap: 8px;
}
.skeleton-item {
height: 40px;
background: linear-gradient(
90deg,
var(--color-bg-tertiary, #1a1a1a) 25%,
var(--color-bg-secondary, #111) 50%,
var(--color-bg-tertiary, #1a1a1a) 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 8px;
}
.skeleton-item.small {
height: 32px;
}
@keyframes skeleton-loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.grid-loading {
display: contents;
}
.dashboard-card-skeleton {
background: var(--color-bg-secondary, #111);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 12px;
overflow: hidden;
}
.skeleton-header {
height: 60px;
background: linear-gradient(
90deg,
var(--color-bg-tertiary, #1a1a1a) 25%,
var(--color-bg-secondary, #111) 50%,
var(--color-bg-tertiary, #1a1a1a) 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
}
.skeleton-body {
padding: 16px;
}
.skeleton-chart {
height: 140px;
background: linear-gradient(
90deg,
var(--color-bg-tertiary, #1a1a1a) 25%,
var(--color-bg-secondary, #111) 50%,
var(--color-bg-tertiary, #1a1a1a) 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s infinite;
border-radius: 8px;
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid var(--color-border, #2a2a2a);
border-top-color: var(--color-accent, #d4f505);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loading-spinner.small {
width: 16px;
height: 16px;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.notification {
position: fixed;
bottom: 24px;
right: 24px;
padding: 14px 20px;
background: var(--color-bg-secondary, #111);
border: 1px solid var(--color-border, #2a2a2a);
border-radius: 8px;
font-size: 14px;
z-index: 1100;
animation: slideIn 0.3s ease;
}
.notification.success {
border-color: #10b981;
color: #10b981;
}
.notification.error {
border-color: #ef4444;
color: #ef4444;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.connection-fields {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--color-border, #2a2a2a);
}
@media (max-width: 768px) {
.dashboards-header {
flex-direction: column;
gap: 16px;
align-items: stretch;
}
.dashboards-header .header-actions {
flex-wrap: wrap;
}
.dashboards-header .search-box input {
width: 100%;
}
.dashboards-layout {
flex-direction: column;
}
.dashboards-sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--color-border, #2a2a2a);
max-height: 200px;
}
.dashboards-grid {
grid-template-columns: 1fr;
}
.form-row {
flex-direction: column;
gap: 0;
}
.template-selection .template-options {
justify-content: center;
}
.widget-options {
justify-content: center;
}
}

File diff suppressed because it is too large Load diff

View file

@ -31,6 +31,7 @@
<link rel="stylesheet" href="tasks/tasks.css?v=20251230" /> <link rel="stylesheet" href="tasks/tasks.css?v=20251230" />
<link rel="stylesheet" href="tasks/taskmd.css?v=20251230" /> <link rel="stylesheet" href="tasks/taskmd.css?v=20251230" />
<link rel="stylesheet" href="analytics/analytics.css" /> <link rel="stylesheet" href="analytics/analytics.css" />
<link rel="stylesheet" href="dashboards/dashboards.css" />
<link rel="stylesheet" href="monitoring/monitoring.css" /> <link rel="stylesheet" href="monitoring/monitoring.css" />
<!-- Local Libraries (no external CDN dependencies) --> <!-- Local Libraries (no external CDN dependencies) -->
@ -351,6 +352,31 @@
</svg> </svg>
<span data-i18n="nav-slides">Apresentações</span> <span data-i18n="nav-slides">Apresentações</span>
</a> </a>
<a
class="app-tab"
href="#social"
data-section="social"
hx-get="/suite/social/social.html"
hx-target="#main-content"
hx-push-url="/#social"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"
/>
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
<span data-i18n="nav-social">Social</span>
</a>
</nav> </nav>
<!-- Apps menu container (button + dropdown) --> <!-- Apps menu container (button + dropdown) -->
@ -771,6 +797,92 @@
<span data-i18n="nav-analytics">Analytics</span> <span data-i18n="nav-analytics">Analytics</span>
</a> </a>
<!-- Dashboards -->
<a
class="app-item"
href="#dashboards"
data-section="dashboards"
role="menuitem"
aria-label="Custom Dashboards"
hx-get="/suite/dashboards/dashboards.html"
hx-target="#main-content"
hx-push-url="/#dashboards"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="3"
y="3"
width="7"
height="7"
rx="1"
/>
<rect
x="14"
y="3"
width="7"
height="7"
rx="1"
/>
<rect
x="3"
y="14"
width="7"
height="7"
rx="1"
/>
<rect
x="14"
y="14"
width="7"
height="7"
rx="1"
/>
</svg>
</div>
<span data-i18n="nav-dashboards"
>Dashboards</span
>
</a>
<!-- Social -->
<a
class="app-item"
href="#social"
data-section="social"
role="menuitem"
aria-label="Social Network"
hx-get="/suite/social/social.html"
hx-target="#main-content"
hx-push-url="/#social"
>
<div class="app-icon" aria-hidden="true">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"
/>
<circle cx="9" cy="7" r="4" />
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
</div>
<span data-i18n="nav-social">Social</span>
</a>
<!-- Monitoring --> <!-- Monitoring -->
<a <a
class="app-item" class="app-item"

View file

@ -21,6 +21,7 @@
"nav-meet": "Meet", "nav-meet": "Meet",
"nav-research": "Research", "nav-research": "Research",
"nav-analytics": "Analytics", "nav-analytics": "Analytics",
"nav-dashboards": "Dashboards",
"nav-monitoring": "Monitoring", "nav-monitoring": "Monitoring",
"nav-admin": "Admin", "nav-admin": "Admin",
"nav-sources": "Sources", "nav-sources": "Sources",
@ -347,6 +348,77 @@
"compliance-filter-severity": "Severity:", "compliance-filter-severity": "Severity:",
"compliance-filter-type": "Type:", "compliance-filter-type": "Type:",
"compliance-issues-found": "{count} issues found", "compliance-issues-found": "{count} issues found",
"dashboards-title": "Dashboards",
"dashboards-search-placeholder": "Search dashboards...",
"dashboards-filter-all": "All Dashboards",
"dashboards-filter-sales": "Sales",
"dashboards-filter-marketing": "Marketing",
"dashboards-filter-operations": "Operations",
"dashboards-filter-finance": "Finance",
"dashboards-filter-hr": "HR",
"dashboards-create": "New Dashboard",
"dashboards-my-dashboards": "My Dashboards",
"dashboards-shared": "Shared With Me",
"dashboards-templates": "Templates",
"dashboards-data-sources": "Data Sources",
"dashboards-add-source": "Add Data Source",
"dashboards-today": "Today",
"dashboards-last-7d": "Last 7 Days",
"dashboards-last-30d": "Last 30 Days",
"dashboards-last-90d": "Last 90 Days",
"dashboards-last-year": "Last Year",
"dashboards-custom-range": "Custom Range",
"dashboards-ask-data": "Ask about your data",
"dashboards-query-placeholder":
"e.g., Show me sales by region for last quarter...",
"dashboards-all-sources": "All Data Sources",
"dashboards-analyzing": "Analyzing your data...",
"dashboards-create-title": "Create New Dashboard",
"dashboards-name": "Name",
"dashboards-name-placeholder": "My Dashboard",
"dashboards-description": "Description",
"dashboards-description-placeholder":
"Describe what this dashboard shows...",
"dashboards-layout": "Layout",
"dashboards-layout-12col": "12 Columns",
"dashboards-layout-6col": "6 Columns",
"dashboards-layout-4col": "4 Columns",
"dashboards-tags": "Tags",
"dashboards-tags-placeholder": "sales, marketing",
"dashboards-public": "Make dashboard public",
"dashboards-start-from": "Start from",
"dashboards-blank": "Blank",
"dashboards-sales-template": "Sales",
"dashboards-marketing-template": "Marketing",
"dashboards-operations-template": "Operations",
"dashboards-add-data-source": "Add Data Source",
"dashboards-source-name": "Name",
"dashboards-source-name-placeholder": "My Database",
"dashboards-source-type": "Type",
"dashboards-select-type": "Select type...",
"dashboards-databases": "Databases",
"dashboards-warehouses": "Cloud Data Warehouses",
"dashboards-apis": "APIs",
"dashboards-files": "Files",
"dashboards-internal": "Internal",
"dashboards-source-description": "Description",
"dashboards-source-description-placeholder": "Optional description...",
"dashboards-test-connection": "Test Connection",
"dashboards-add-widget": "Add Widget",
"dashboards-charts": "Charts",
"dashboards-data-display": "Data Display",
"dashboards-content": "Content",
"dashboards-filters": "Filters",
"dashboards-host": "Host",
"dashboards-port": "Port",
"dashboards-database": "Database",
"dashboards-username": "Username",
"dashboards-password": "Password",
"dashboards-use-ssl": "Use SSL",
"dashboards-api-url": "API URL",
"dashboards-api-key": "API Key",
"dashboards-file-url": "File URL or Path",
"dashboards-connection-string": "Connection String",
"sources-title": "Sources", "sources-title": "Sources",
"sources-subtitle": "Manage data sources, prompts, and integrations", "sources-subtitle": "Manage data sources, prompts, and integrations",
"sources-search": "Search sources...", "sources-search": "Search sources...",
@ -404,7 +476,8 @@
"nav-tasks": "Tarefas", "nav-tasks": "Tarefas",
"nav-meet": "Reuniões", "nav-meet": "Reuniões",
"nav-research": "Pesquisa", "nav-research": "Pesquisa",
"nav-analytics": "Análises", "nav-analytics": "Analytics",
"nav-dashboards": "Painéis",
"nav-monitoring": "Monitoramento", "nav-monitoring": "Monitoramento",
"nav-admin": "Administração", "nav-admin": "Administração",
"nav-sources": "Fontes", "nav-sources": "Fontes",
@ -735,6 +808,77 @@
"compliance-filter-severity": "Severidade:", "compliance-filter-severity": "Severidade:",
"compliance-filter-type": "Tipo:", "compliance-filter-type": "Tipo:",
"compliance-issues-found": "{count} problemas encontrados", "compliance-issues-found": "{count} problemas encontrados",
"dashboards-title": "Painéis",
"dashboards-search-placeholder": "Buscar painéis...",
"dashboards-filter-all": "Todos os Painéis",
"dashboards-filter-sales": "Vendas",
"dashboards-filter-marketing": "Marketing",
"dashboards-filter-operations": "Operações",
"dashboards-filter-finance": "Finanças",
"dashboards-filter-hr": "RH",
"dashboards-create": "Novo Painel",
"dashboards-my-dashboards": "Meus Painéis",
"dashboards-shared": "Compartilhados Comigo",
"dashboards-templates": "Modelos",
"dashboards-data-sources": "Fontes de Dados",
"dashboards-add-source": "Adicionar Fonte de Dados",
"dashboards-today": "Hoje",
"dashboards-last-7d": "Últimos 7 Dias",
"dashboards-last-30d": "Últimos 30 Dias",
"dashboards-last-90d": "Últimos 90 Dias",
"dashboards-last-year": "Último Ano",
"dashboards-custom-range": "Período Personalizado",
"dashboards-ask-data": "Pergunte sobre seus dados",
"dashboards-query-placeholder":
"Ex: Mostre vendas por região no último trimestre...",
"dashboards-all-sources": "Todas as Fontes de Dados",
"dashboards-analyzing": "Analisando seus dados...",
"dashboards-create-title": "Criar Novo Painel",
"dashboards-name": "Nome",
"dashboards-name-placeholder": "Meu Painel",
"dashboards-description": "Descrição",
"dashboards-description-placeholder":
"Descreva o que este painel mostra...",
"dashboards-layout": "Layout",
"dashboards-layout-12col": "12 Colunas",
"dashboards-layout-6col": "6 Colunas",
"dashboards-layout-4col": "4 Colunas",
"dashboards-tags": "Tags",
"dashboards-tags-placeholder": "vendas, marketing",
"dashboards-public": "Tornar painel público",
"dashboards-start-from": "Começar de",
"dashboards-blank": "Em Branco",
"dashboards-sales-template": "Vendas",
"dashboards-marketing-template": "Marketing",
"dashboards-operations-template": "Operações",
"dashboards-add-data-source": "Adicionar Fonte de Dados",
"dashboards-source-name": "Nome",
"dashboards-source-name-placeholder": "Meu Banco de Dados",
"dashboards-source-type": "Tipo",
"dashboards-select-type": "Selecione o tipo...",
"dashboards-databases": "Bancos de Dados",
"dashboards-warehouses": "Data Warehouses na Nuvem",
"dashboards-apis": "APIs",
"dashboards-files": "Arquivos",
"dashboards-internal": "Interno",
"dashboards-source-description": "Descrição",
"dashboards-source-description-placeholder": "Descrição opcional...",
"dashboards-test-connection": "Testar Conexão",
"dashboards-add-widget": "Adicionar Widget",
"dashboards-charts": "Gráficos",
"dashboards-data-display": "Exibição de Dados",
"dashboards-content": "Conteúdo",
"dashboards-filters": "Filtros",
"dashboards-host": "Host",
"dashboards-port": "Porta",
"dashboards-database": "Banco de Dados",
"dashboards-username": "Usuário",
"dashboards-password": "Senha",
"dashboards-use-ssl": "Usar SSL",
"dashboards-api-url": "URL da API",
"dashboards-api-key": "Chave da API",
"dashboards-file-url": "URL ou Caminho do Arquivo",
"dashboards-connection-string": "String de Conexão",
"sources-title": "Fontes", "sources-title": "Fontes",
"sources-subtitle": "Gerenciar fontes de dados, prompts e integrações", "sources-subtitle": "Gerenciar fontes de dados, prompts e integrações",
"sources-search": "Buscar fontes...", "sources-search": "Buscar fontes...",

1249
ui/suite/learn/learn.css Normal file

File diff suppressed because it is too large Load diff

563
ui/suite/learn/learn.html Normal file
View file

@ -0,0 +1,563 @@
<div class="learn-container">
<!-- Sidebar -->
<aside class="learn-sidebar">
<div class="sidebar-header">
<h2 data-i18n="learn-title">📚 Learn</h2>
<button class="btn-icon-sm" onclick="toggleLearnSidebar()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
</div>
<!-- User Stats Card -->
<div class="user-stats-card">
<div class="stats-header">
<span class="stats-icon">🎓</span>
<span data-i18n="learn-my-progress">Meu Progresso</span>
</div>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value" id="statCoursesCompleted">0</span>
<span class="stat-label" data-i18n="learn-completed">Concluídos</span>
</div>
<div class="stat-item">
<span class="stat-value" id="statCoursesInProgress">0</span>
<span class="stat-label" data-i18n="learn-in-progress">Em Andamento</span>
</div>
<div class="stat-item">
<span class="stat-value" id="statCertificates">0</span>
<span class="stat-label" data-i18n="learn-certificates">Certificados</span>
</div>
<div class="stat-item">
<span class="stat-value" id="statTimeSpent">0h</span>
<span class="stat-label" data-i18n="learn-time-spent">Tempo Total</span>
</div>
</div>
</div>
<!-- Categories -->
<div class="sidebar-section">
<h3 data-i18n="learn-categories">Categorias</h3>
<div class="category-list" id="categoryList">
<button class="category-item active" data-category="all">
<span class="category-icon">📚</span>
<span data-i18n="learn-all-courses">Todos os Cursos</span>
<span class="category-count" id="countAll">0</span>
</button>
<button class="category-item" data-category="mandatory">
<span class="category-icon">⚠️</span>
<span data-i18n="learn-mandatory">Obrigatórios</span>
<span class="category-count" id="countMandatory">0</span>
</button>
<button class="category-item" data-category="compliance">
<span class="category-icon">📋</span>
<span data-i18n="learn-compliance">Compliance</span>
<span class="category-count" id="countCompliance">0</span>
</button>
<button class="category-item" data-category="security">
<span class="category-icon">🔒</span>
<span data-i18n="learn-security">Segurança</span>
<span class="category-count" id="countSecurity">0</span>
</button>
<button class="category-item" data-category="skills">
<span class="category-icon">💡</span>
<span data-i18n="learn-skills">Habilidades</span>
<span class="category-count" id="countSkills">0</span>
</button>
<button class="category-item" data-category="onboarding">
<span class="category-icon">🚀</span>
<span data-i18n="learn-onboarding">Integração</span>
<span class="category-count" id="countOnboarding">0</span>
</button>
</div>
</div>
<!-- Difficulty Filter -->
<div class="sidebar-section">
<h3 data-i18n="learn-difficulty">Dificuldade</h3>
<div class="difficulty-filter">
<label class="checkbox-label">
<input type="checkbox" checked data-difficulty="beginner">
<span class="difficulty-badge beginner">Iniciante</span>
</label>
<label class="checkbox-label">
<input type="checkbox" checked data-difficulty="intermediate">
<span class="difficulty-badge intermediate">Intermediário</span>
</label>
<label class="checkbox-label">
<input type="checkbox" checked data-difficulty="advanced">
<span class="difficulty-badge advanced">Avançado</span>
</label>
</div>
</div>
<!-- My Certificates -->
<div class="sidebar-section">
<div class="section-header">
<h3 data-i18n="learn-my-certificates">Meus Certificados</h3>
<button class="btn-link" onclick="showAllCertificates()">
<span data-i18n="learn-view-all">Ver todos</span>
</button>
</div>
<div class="certificates-preview" id="certificatesPreview">
<div class="empty-state-small">
<span>🏆</span>
<p data-i18n="learn-no-certificates">Nenhum certificado ainda</p>
</div>
</div>
</div>
</aside>
<!-- Main Content -->
<main class="learn-main">
<!-- Header -->
<div class="learn-header">
<div class="header-left">
<div class="tabs">
<button class="tab active" data-tab="catalog" onclick="switchTab('catalog')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
<span data-i18n="learn-catalog">Catálogo</span>
</button>
<button class="tab" data-tab="my-courses" onclick="switchTab('my-courses')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 10v6M2 10l10-5 10 5-10 5z"></path>
<path d="M6 12v5c3 3 9 3 12 0v-5"></path>
</svg>
<span data-i18n="learn-my-courses">Meus Cursos</span>
<span class="badge" id="myCoursesCount">0</span>
</button>
<button class="tab" data-tab="mandatory" onclick="switchTab('mandatory')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<span data-i18n="learn-pending">Pendentes</span>
<span class="badge warning" id="mandatoryCount">0</span>
</button>
<button class="tab" data-tab="certificates" onclick="switchTab('certificates')">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="8" r="7"></circle>
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"></polyline>
</svg>
<span data-i18n="learn-certificates">Certificados</span>
</button>
</div>
</div>
<div class="header-right">
<div class="search-box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input type="text" id="searchCourses" placeholder="Buscar cursos..." data-i18n-placeholder="learn-search-placeholder">
</div>
<select id="sortCourses" class="sort-select">
<option value="recent" data-i18n="learn-sort-recent">Mais Recentes</option>
<option value="popular" data-i18n="learn-sort-popular">Mais Populares</option>
<option value="duration-asc" data-i18n="learn-sort-duration-asc">Menor Duração</option>
<option value="duration-desc" data-i18n="learn-sort-duration-desc">Maior Duração</option>
</select>
</div>
</div>
<!-- Mandatory Training Alert -->
<div class="mandatory-alert" id="mandatoryAlert" style="display: none;">
<div class="alert-icon">⚠️</div>
<div class="alert-content">
<strong data-i18n="learn-mandatory-alert-title">Treinamentos Obrigatórios Pendentes</strong>
<p id="mandatoryAlertText">Você possui treinamentos obrigatórios com prazo próximo.</p>
</div>
<button class="btn-primary-sm" onclick="switchTab('mandatory')">
<span data-i18n="learn-view-pending">Ver Pendentes</span>
</button>
</div>
<!-- Tab Content: Catalog -->
<div class="tab-content active" id="tab-catalog">
<!-- Recommended Section -->
<section class="courses-section">
<div class="section-header">
<h3>
<span class="section-icon"></span>
<span data-i18n="learn-recommended">Recomendados para Você</span>
</h3>
</div>
<div class="courses-carousel" id="recommendedCourses">
<!-- Courses loaded dynamically -->
</div>
</section>
<!-- All Courses Grid -->
<section class="courses-section">
<div class="section-header">
<h3>
<span class="section-icon">📚</span>
<span data-i18n="learn-all-courses">Todos os Cursos</span>
</h3>
<span class="courses-count" id="coursesCountLabel">0 cursos</span>
</div>
<div class="courses-grid" id="coursesGrid">
<!-- Courses loaded dynamically -->
</div>
<div class="load-more" id="loadMore" style="display: none;">
<button class="btn-secondary" onclick="loadMoreCourses()">
<span data-i18n="learn-load-more">Carregar Mais</span>
</button>
</div>
</section>
</div>
<!-- Tab Content: My Courses -->
<div class="tab-content" id="tab-my-courses">
<!-- Continue Learning -->
<section class="courses-section">
<div class="section-header">
<h3>
<span class="section-icon">▶️</span>
<span data-i18n="learn-continue">Continuar Aprendendo</span>
</h3>
</div>
<div class="courses-list" id="continueLearning">
<!-- In progress courses -->
</div>
</section>
<!-- Completed Courses -->
<section class="courses-section">
<div class="section-header">
<h3>
<span class="section-icon"></span>
<span data-i18n="learn-completed-courses">Cursos Concluídos</span>
</h3>
</div>
<div class="courses-list" id="completedCourses">
<!-- Completed courses -->
</div>
</section>
</div>
<!-- Tab Content: Mandatory -->
<div class="tab-content" id="tab-mandatory">
<section class="courses-section">
<div class="section-header">
<h3>
<span class="section-icon">⚠️</span>
<span data-i18n="learn-mandatory-training">Treinamentos Obrigatórios</span>
</h3>
</div>
<div class="mandatory-list" id="mandatoryList">
<!-- Mandatory courses -->
</div>
</section>
</div>
<!-- Tab Content: Certificates -->
<div class="tab-content" id="tab-certificates">
<section class="courses-section">
<div class="section-header">
<h3>
<span class="section-icon">🏆</span>
<span data-i18n="learn-my-certificates">Meus Certificados</span>
</h3>
</div>
<div class="certificates-grid" id="certificatesGrid">
<!-- Certificates -->
</div>
</section>
</div>
</main>
<!-- Course Detail Modal -->
<div class="modal hidden" id="courseModal">
<div class="modal-content modal-lg">
<div class="modal-header">
<h3 id="modalCourseTitle">Título do Curso</h3>
<button class="btn-icon-sm" onclick="closeCourseModal()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="course-detail">
<div class="course-detail-header">
<div class="course-thumbnail" id="modalThumbnail">
<img src="" alt="Course thumbnail">
</div>
<div class="course-info">
<div class="course-meta">
<span class="difficulty-badge" id="modalDifficulty">Iniciante</span>
<span class="duration" id="modalDuration">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span>30 min</span>
</span>
<span class="lessons-count" id="modalLessonsCount">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
</svg>
<span>5 aulas</span>
</span>
</div>
<p class="course-description" id="modalDescription">
Descrição do curso...
</p>
<div class="course-progress" id="modalProgress" style="display: none;">
<div class="progress-bar">
<div class="progress-fill" id="modalProgressFill" style="width: 0%"></div>
</div>
<span class="progress-text" id="modalProgressText">0% completo</span>
</div>
</div>
</div>
<!-- Lessons List -->
<div class="lessons-section">
<h4 data-i18n="learn-lessons">Aulas</h4>
<div class="lessons-list" id="modalLessonsList">
<!-- Lessons loaded dynamically -->
</div>
</div>
<!-- Quiz Section -->
<div class="quiz-section" id="modalQuizSection" style="display: none;">
<h4 data-i18n="learn-quiz">Avaliação</h4>
<div class="quiz-info">
<div class="quiz-meta">
<span id="modalQuizQuestions">10 questões</span>
<span id="modalQuizTime">15 min</span>
<span id="modalQuizPassing">70% para aprovação</span>
</div>
<button class="btn-primary" id="startQuizBtn" onclick="startQuiz()">
<span data-i18n="learn-start-quiz">Iniciar Avaliação</span>
</button>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeCourseModal()">
<span data-i18n="learn-close">Fechar</span>
</button>
<button class="btn-primary" id="startCourseBtn" onclick="startCourse()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
<span data-i18n="learn-start-course">Iniciar Curso</span>
</button>
</div>
</div>
</div>
<!-- Quiz Modal -->
<div class="modal hidden" id="quizModal">
<div class="modal-content modal-lg">
<div class="modal-header">
<div class="quiz-header-info">
<h3 id="quizTitle">Avaliação</h3>
<div class="quiz-timer" id="quizTimer">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
<span id="quizTimeRemaining">15:00</span>
</div>
</div>
<button class="btn-icon-sm" onclick="confirmExitQuiz()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="quiz-progress">
<div class="quiz-progress-bar">
<div class="quiz-progress-fill" id="quizProgressFill" style="width: 0%"></div>
</div>
<span class="quiz-progress-text" id="quizProgressText">Questão 1 de 10</span>
</div>
<div class="quiz-question" id="quizQuestion">
<!-- Question loaded dynamically -->
</div>
<div class="quiz-options" id="quizOptions">
<!-- Options loaded dynamically -->
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" id="prevQuestionBtn" onclick="prevQuestion()" disabled>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
<span data-i18n="learn-previous">Anterior</span>
</button>
<button class="btn-primary" id="nextQuestionBtn" onclick="nextQuestion()">
<span data-i18n="learn-next">Próxima</span>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
<button class="btn-primary" id="submitQuizBtn" onclick="submitQuiz()" style="display: none;">
<span data-i18n="learn-submit">Enviar Respostas</span>
</button>
</div>
</div>
</div>
<!-- Quiz Result Modal -->
<div class="modal hidden" id="quizResultModal">
<div class="modal-content">
<div class="modal-header">
<h3 data-i18n="learn-quiz-result">Resultado da Avaliação</h3>
<button class="btn-icon-sm" onclick="closeQuizResult()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="quiz-result" id="quizResult">
<div class="result-icon" id="resultIcon">🎉</div>
<h2 class="result-title" id="resultTitle">Parabéns!</h2>
<p class="result-message" id="resultMessage">Você passou na avaliação!</p>
<div class="result-score">
<div class="score-circle" id="scoreCircle">
<span class="score-value" id="scoreValue">85%</span>
</div>
<div class="score-details">
<div class="score-detail">
<span class="detail-label" data-i18n="learn-correct-answers">Acertos</span>
<span class="detail-value" id="correctAnswers">8/10</span>
</div>
<div class="score-detail">
<span class="detail-label" data-i18n="learn-time-taken">Tempo</span>
<span class="detail-value" id="timeTaken">12:30</span>
</div>
<div class="score-detail">
<span class="detail-label" data-i18n="learn-attempt">Tentativa</span>
<span class="detail-value" id="attemptNumber">1</span>
</div>
</div>
</div>
<div class="result-certificate" id="resultCertificate" style="display: none;">
<p data-i18n="learn-certificate-earned">🏆 Certificado conquistado!</p>
<button class="btn-secondary" onclick="downloadCertificate()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
<span data-i18n="learn-download-certificate">Baixar Certificado</span>
</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="reviewAnswers()">
<span data-i18n="learn-review-answers">Revisar Respostas</span>
</button>
<button class="btn-primary" onclick="closeQuizResult()">
<span data-i18n="learn-continue">Continuar</span>
</button>
</div>
</div>
</div>
<!-- Lesson Viewer Modal -->
<div class="modal hidden" id="lessonModal">
<div class="modal-content modal-fullscreen">
<div class="modal-header">
<div class="lesson-nav">
<button class="btn-icon-sm" onclick="prevLesson()" id="prevLessonBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<span id="lessonNavTitle">Aula 1 de 5</span>
<button class="btn-icon-sm" onclick="nextLesson()" id="nextLessonBtn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
<h3 id="lessonTitle">Título da Aula</h3>
<button class="btn-icon-sm" onclick="closeLessonModal()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body lesson-body">
<div class="lesson-content" id="lessonContent">
<!-- Lesson content (video, text, slides) -->
</div>
<div class="lesson-sidebar">
<h4 data-i18n="learn-course-content">Conteúdo do Curso</h4>
<div class="lesson-list-sidebar" id="lessonListSidebar">
<!-- Lesson list -->
</div>
</div>
</div>
<div class="modal-footer">
<div class="lesson-progress">
<div class="progress-bar">
<div class="progress-fill" id="lessonProgressFill" style="width: 0%"></div>
</div>
</div>
<button class="btn-primary" id="completeLessonBtn" onclick="completeLesson()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span data-i18n="learn-mark-complete">Marcar como Concluída</span>
</button>
</div>
</div>
</div>
<!-- Certificate Modal -->
<div class="modal hidden" id="certificateModal">
<div class="modal-content">
<div class="modal-header">
<h3 data-i18n="learn-certificate">Certificado</h3>
<button class="btn-icon-sm" onclick="closeCertificateModal()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="modal-body">
<div class="certificate-preview" id="certificatePreview">
<div class="certificate">
<div class="certificate-header">
<span class="certificate-logo">🎓</span>
<h2>General Bots</h2>
<p>Certificado de Conclusão</p>
</div>
<div class="certificate-body">
<p>Certificamos que</p>
<h3 id="certUserName">Nome do Usuário</h3>
<p>concluiu com sucesso o curso</p>
<h4 id="certCourseName">Nome do Curso</h4>
<p class="cert-score">com aproveitamento de <strong id="certScore">85%</strong></p>
</div>
<div class="certificate-footer">
<div class="cert-date">
<span data-i18n="learn-issued-on">Emitido em</span>
<strong id="certDate">01/01/2025</strong>
</div>
<div class="cert-code">
<span data-i18n="learn-verification-code">Código de Verificação

824
ui/suite/learn/learn.js Normal file
View file

@ -0,0 +1,824 @@
/**
* Learn Module - JavaScript Controller
* Learning Management System for General Bots
*/
// State management
const LearnState = {
courses: [],
myCourses: [],
mandatoryAssignments: [],
certificates: [],
categories: [],
currentCourse: null,
currentLesson: null,
currentQuiz: null,
quizAnswers: {},
quizTimer: null,
quizTimeRemaining: 0,
currentQuestionIndex: 0,
filters: {
category: 'all',
difficulty: ['beginner', 'intermediate', 'advanced'],
search: '',
sort: 'recent'
},
pagination: {
offset: 0,
limit: 12,
hasMore: true
},
userStats: {
coursesCompleted: 0,
coursesInProgress: 0,
certificates: 0,
timeSpent: 0
}
};
// API Base URL
const LEARN_API = '/api/learn';
// ============================================================================
// INITIALIZATION
// ============================================================================
document.addEventListener('DOMContentLoaded', () => {
initLearn();
});
function initLearn() {
loadUserStats();
loadCategories();
loadCourses();
loadMyCourses();
loadMandatoryAssignments();
loadCertificates();
loadRecommendations();
bindEvents();
}
function bindEvents() {
// Search input
const searchInput = document.getElementById('searchCourses');
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
LearnState.filters.search = e.target.value;
LearnState.pagination.offset = 0;
loadCourses();
}, 300);
});
}
// Sort select
const sortSelect = document.getElementById('sortCourses');
if (sortSelect) {
sortSelect.addEventListener('change', (e) => {
LearnState.filters.sort = e.target.value;
LearnState.pagination.offset = 0;
loadCourses();
});
}
// Category filters
document.querySelectorAll('.category-item').forEach(item => {
item.addEventListener('click', () => {
document.querySelectorAll('.category-item').forEach(i => i.classList.remove('active'));
item.classList.add('active');
LearnState.filters.category = item.dataset.category;
LearnState.pagination.offset = 0;
loadCourses();
});
});
// Difficulty filters
document.querySelectorAll('[data-difficulty]').forEach(checkbox => {
checkbox.addEventListener('change', () => {
LearnState.filters.difficulty = Array.from(
document.querySelectorAll('[data-difficulty]:checked')
).map(cb => cb.dataset.difficulty);
LearnState.pagination.offset = 0;
loadCourses();
});
});
// Close modals on background click
document.querySelectorAll('.modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.add('hidden');
}
});
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeAllModals();
}
});
}
// ============================================================================
// API CALLS
// ============================================================================
async function apiCall(endpoint, options = {}) {
try {
const response = await fetch(`${LEARN_API}${endpoint}`, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'API request failed');
}
return data;
} catch (error) {
console.error('API Error:', error);
showNotification('error', 'Erro ao carregar dados. Tente novamente.');
throw error;
}
}
async function loadUserStats() {
try {
const response = await apiCall('/stats/user');
if (response.success) {
LearnState.userStats = response.data;
updateUserStatsUI();
}
} catch (error) {
// Use mock data if API fails
updateUserStatsUI();
}
}
async function loadCategories() {
try {
const response = await apiCall('/categories');
if (response.success) {
LearnState.categories = response.data;
updateCategoryCounts();
}
} catch (error) {
// Categories loaded from HTML
}
}
async function loadCourses() {
try {
const params = new URLSearchParams({
limit: LearnState.pagination.limit,
offset: LearnState.pagination.offset
});
if (LearnState.filters.category !== 'all') {
params.append('category', LearnState.filters.category);
}
if (LearnState.filters.search) {
params.append('search', LearnState.filters.search);
}
if (LearnState.filters.difficulty.length < 3) {
params.append('difficulty', LearnState.filters.difficulty.join(','));
}
const response = await apiCall(`/courses?${params}`);
if (response.success) {
if (LearnState.pagination.offset === 0) {
LearnState.courses = response.data;
} else {
LearnState.courses = [...LearnState.courses, ...response.data];
}
LearnState.pagination.hasMore = response.data.length >= LearnState.pagination.limit;
renderCourses();
}
} catch (error) {
// Render mock courses for demo
renderMockCourses();
}
}
async function loadMyCourses() {
try {
const response = await apiCall('/progress');
if (response.success) {
LearnState.myCourses = response.data;
renderMyCourses();
}
} catch (error) {
renderMockMyCourses();
}
}
async function loadMandatoryAssignments() {
try {
const response = await apiCall('/assignments/pending');
if (response.success) {
LearnState.mandatoryAssignments = response.data;
renderMandatoryAssignments();
updateMandatoryAlert();
}
} catch (error) {
renderMockMandatory();
}
}
async function loadCertificates() {
try {
const response = await apiCall('/certificates');
if (response.success) {
LearnState.certificates = response.data;
renderCertificates();
}
} catch (error) {
renderMockCertificates();
}
}
async function loadRecommendations() {
try {
const response = await apiCall('/recommendations');
if (response.success) {
renderRecommendations(response.data);
}
} catch (error) {
renderMockRecommendations();
}
}
async function loadCourseDetail(courseId) {
try {
const response = await apiCall(`/courses/${courseId}`);
if (response.success) {
LearnState.currentCourse = response.data;
renderCourseModal(response.data);
}
} catch (error) {
showMockCourseDetail(courseId);
}
}
async function startCourseAPI(courseId) {
try {
const response = await apiCall(`/progress/${courseId}/start`, {
method: 'POST'
});
if (response.success) {
showNotification('success', 'Curso iniciado com sucesso!');
loadMyCourses();
return response.data;
}
} catch (error) {
showNotification('error', 'Erro ao iniciar o curso.');
}
}
async function completeLessonAPI(lessonId) {
try {
const response = await apiCall(`/progress/${lessonId}/complete`, {
method: 'POST'
});
if (response.success) {
showNotification('success', 'Aula concluída!');
return response.data;
}
} catch (error) {
showNotification('error', 'Erro ao marcar aula como concluída.');
}
}
async function submitQuizAPI(courseId, answers) {
try {
const response = await apiCall(`/courses/${courseId}/quiz`, {
method: 'POST',
body: JSON.stringify({ answers })
});
if (response.success) {
return response.data;
}
} catch (error) {
showNotification('error', 'Erro ao enviar respostas.');
}
}
// ============================================================================
// UI RENDERING
// ============================================================================
function updateUserStatsUI() {
document.getElementById('statCoursesCompleted').textContent = LearnState.userStats.courses_completed || 0;
document.getElementById('statCoursesInProgress').textContent = LearnState.userStats.courses_in_progress || 0;
document.getElementById('statCertificates').textContent = LearnState.userStats.certificates_earned || 0;
document.getElementById('statTimeSpent').textContent = `${LearnState.userStats.total_time_spent_hours || 0}h`;
}
function updateCategoryCounts() {
// Update category counts based on courses
const counts = {};
LearnState.courses.forEach(course => {
counts[course.category] = (counts[course.category] || 0) + 1;
});
document.getElementById('countAll').textContent = LearnState.courses.length;
document.getElementById('countMandatory').textContent = LearnState.mandatoryAssignments.length;
}
function renderCourses() {
const grid = document.getElementById('coursesGrid');
const countLabel = document.getElementById('coursesCountLabel');
if (!grid) return;
if (LearnState.pagination.offset === 0) {
grid.innerHTML = '';
}
if (LearnState.courses.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<span>📚</span>
<h3>Nenhum curso encontrado</h3>
<p>Tente ajustar os filtros de busca.</p>
</div>
`;
countLabel.textContent = '0 cursos';
return;
}
LearnState.courses.forEach(course => {
grid.appendChild(createCourseCard(course));
});
countLabel.textContent = `${LearnState.courses.length} cursos`;
// Show/hide load more button
const loadMore = document.getElementById('loadMore');
if (loadMore) {
loadMore.style.display = LearnState.pagination.hasMore ? 'block' : 'none';
}
}
function createCourseCard(course) {
const card = document.createElement('div');
card.className = 'course-card';
card.onclick = () => openCourseModal(course.id);
const difficultyClass = (course.difficulty || 'beginner').toLowerCase();
const progress = course.user_progress || 0;
card.innerHTML = `
<div class="course-thumbnail">
${course.thumbnail_url
? `<img src="${course.thumbnail_url}" alt="${course.title}">`
: `<span class="placeholder-icon">📖</span>`
}
${course.is_mandatory ? '<span class="course-mandatory-badge">Obrigatório</span>' : ''}
${progress > 0 ? `<span class="course-progress-badge">${progress}%</span>` : ''}
</div>
<div class="course-content">
<h3 class="course-title">${escapeHtml(course.title)}</h3>
<div class="course-meta">
<span class="difficulty-badge ${difficultyClass}">${formatDifficulty(course.difficulty)}</span>
<span>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
${formatDuration(course.duration_minutes)}
</span>
</div>
${progress > 0 ? `
<div class="course-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${progress}%"></div>
</div>
<span class="progress-text">${progress}% completo</span>
</div>
` : ''}
</div>
`;
return card;
}
function renderMyCourses() {
const continueLearning = document.getElementById('continueLearning');
const completedCourses = document.getElementById('completedCourses');
if (!continueLearning || !completedCourses) return;
const inProgress = LearnState.myCourses.filter(c => c.status === 'in_progress');
const completed = LearnState.myCourses.filter(c => c.status === 'completed');
// Update badge
document.getElementById('myCoursesCount').textContent = inProgress.length;
// Render in-progress courses
if (inProgress.length === 0) {
continueLearning.innerHTML = `
<div class="empty-state-small">
<span>📚</span>
<p>Nenhum curso em andamento</p>
</div>
`;
} else {
continueLearning.innerHTML = inProgress.map(course => createCourseListItem(course)).join('');
}
// Render completed courses
if (completed.length === 0) {
completedCourses.innerHTML = `
<div class="empty-state-small">
<span></span>
<p>Nenhum curso concluído ainda</p>
</div>
`;
} else {
completedCourses.innerHTML = completed.map(course => createCourseListItem(course, true)).join('');
}
}
function createCourseListItem(course, isCompleted = false) {
return `
<div class="course-list-item" onclick="openCourseModal('${course.course_id}')">
<div class="course-list-thumbnail">
<span class="placeholder-icon">📖</span>
</div>
<div class="course-list-info">
<h4>${escapeHtml(course.course_title || course.title || 'Curso')}</h4>
<div class="course-meta">
<span>${formatDuration(course.duration_minutes || 30)}</span>
</div>
</div>
<div class="course-list-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${course.completion_percentage || (isCompleted ? 100 : 0)}%"></div>
</div>
<span class="progress-text">${course.completion_percentage || (isCompleted ? 100 : 0)}% completo</span>
</div>
<div class="course-list-action">
<button class="btn-primary-sm">
${isCompleted ? 'Revisar' : 'Continuar'}
</button>
</div>
</div>
`;
}
function renderMandatoryAssignments() {
const list = document.getElementById('mandatoryList');
const badge = document.getElementById('mandatoryCount');
if (!list) return;
badge.textContent = LearnState.mandatoryAssignments.length;
if (LearnState.mandatoryAssignments.length === 0) {
list.innerHTML = `
<div class="empty-state">
<span>🎉</span>
<h3>Tudo em dia!</h3>
<p>Você não possui treinamentos obrigatórios pendentes.</p>
</div>
`;
return;
}
list.innerHTML = LearnState.mandatoryAssignments.map(assignment => {
const isOverdue = assignment.due_date && new Date(assignment.due_date) < new Date();
const daysUntilDue = assignment.due_date
? Math.ceil((new Date(assignment.due_date) - new Date()) / (1000 * 60 * 60 * 24))
: null;
const isUrgent = daysUntilDue !== null && daysUntilDue <= 7 && daysUntilDue > 0;
return `
<div class="mandatory-item ${isOverdue ? 'overdue' : ''} ${isUrgent ? 'urgent' : ''}"
onclick="openCourseModal('${assignment.course_id}')">
<div class="mandatory-icon">
${isOverdue ? '⚠️' : (isUrgent ? '⏰' : '📋')}
</div>
<div class="mandatory-info">
<h4>${escapeHtml(assignment.course_title || 'Treinamento Obrigatório')}</h4>
<div class="mandatory-due ${isOverdue ? 'overdue' : ''} ${isUrgent ? 'urgent' : ''}">
${isOverdue
? '<span>⚠️ Prazo vencido!</span>'
: (daysUntilDue !== null
? `<span>Prazo: ${daysUntilDue} dias</span>`
: '<span>Sem prazo definido</span>'
)
}
</div>
</div>
<button class="btn-primary">
${isOverdue ? 'Iniciar Agora' : 'Começar'}
</button>
</div>
`;
}).join('');
}
function updateMandatoryAlert() {
const alert = document.getElementById('mandatoryAlert');
const alertText = document.getElementById('mandatoryAlertText');
if (!alert) return;
const overdueCount = LearnState.mandatoryAssignments.filter(a =>
a.due_date && new Date(a.due_date) < new Date()
).length;
const urgentCount = LearnState.mandatoryAssignments.filter(a => {
if (!a.due_date) return false;
const days = Math.ceil((new Date(a.due_date) - new Date()) / (1000 * 60 * 60 * 24));
return days > 0 && days <= 7;
}).length;
if (overdueCount > 0 || urgentCount > 0) {
alert.style.display = 'flex';
if (overdueCount > 0) {
alertText.textContent = `Você possui ${overdueCount} treinamento(s) com prazo vencido!`;
} else {
alertText.textContent = `Você possui ${urgentCount} treinamento(s) com prazo próximo.`;
}
} else {
alert.style.display = 'none';
}
}
function renderCertificates() {
const grid = document.getElementById('certificatesGrid');
const preview = document.getElementById('certificatesPreview');
if (!grid) return;
if (LearnState.certificates.length === 0) {
grid.innerHTML = `
<div class="empty-state">
<span>🏆</span>
<h3>Nenhum certificado ainda</h3>
<p>Complete seus cursos para ganhar certificados.</p>
</div>
`;
if (preview) {
preview.innerHTML = `
<div class="empty-state-small">
<span>🏆</span>
<p>Nenhum certificado ainda</p>
</div>
`;
}
return;
}
grid.innerHTML = LearnState.certificates.map(cert => `
<div class="certificate-card" onclick="openCertificateModal('${cert.id}')">
<div class="certificate-card-header">
<span class="cert-icon">🎓</span>
<h4>${escapeHtml(cert.course_title || 'Curso Concluído')}</h4>
</div>
<div class="certificate-card-body">
<span class="cert-score">${cert.score}%</span>
<span class="cert-date">${formatDate(cert.issued_at)}</span>
</div>
<div class="certificate-card-footer">
<button class="btn-secondary" onclick="event.stopPropagation(); downloadCertificateById('${cert.id}')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Baixar
</button>
<button class="btn-link" onclick="event.stopPropagation(); shareCertificate('${cert.verification_code}')">
Compartilhar
</button>
</div>
</div>
`).join('');
// Update sidebar preview
if (preview) {
preview.innerHTML = LearnState.certificates.slice(0, 3).map(cert => `
<div class="cert-preview-item" onclick="openCertificateModal('${cert.id}')">
<span class="cert-icon">🎓</span>
<div class="cert-info">
<div class="cert-title">${escapeHtml(cert.course_title || 'Curso')}</div>
<div class="cert-date">${formatDate(cert.issued_at)}</div>
</div>
</div>
`).join('');
}
}
function renderRecommendations(courses) {
const carousel = document.getElementById('recommendedCourses');
if (!carousel) return;
if (!courses || courses.length === 0) {
carousel.innerHTML = '<p class="text-secondary">Explore o catálogo para encontrar cursos.</p>';
return;
}
carousel.innerHTML = courses.slice(0, 6).map(course => {
const card = createCourseCard(course);
card.style.minWidth = '280px';
return card.outerHTML;
}).join('');
}
// ============================================================================
// MODALS
// ============================================================================
function openCourseModal(courseId) {
loadCourseDetail(courseId);
document.getElementById('courseModal').classList.remove('hidden');
}
function closeCourseModal() {
document.getElementById('courseModal').classList.add('hidden');
LearnState.currentCourse = null;
}
function renderCourseModal(data) {
const { course, lessons, quiz } = data;
document.getElementById('modalCourseTitle').textContent = course.title;
document.getElementById('modalDescription').textContent = course.description || 'Sem descrição disponível.';
document.getElementById('modalDifficulty').textContent = formatDifficulty(course.difficulty);
document.getElementById('modalDifficulty').className = `difficulty-badge ${(course.difficulty || 'beginner').toLowerCase()}`;
document.getElementById('modalDuration').querySelector('span').textContent = formatDuration(course.duration_minutes);
document.getElementById('modalLessonsCount').querySelector('span').textContent = `${lessons?.length || 0} aulas`;
// Render lessons
const lessonsList = document.getElementById('modalLessonsList');
if (lessons && lessons.length > 0) {
lessonsList.innerHTML = lessons.map((lesson, index) => `
<div class="lesson-item ${lesson.is_completed ? 'completed' : ''}"
onclick="openLesson('${lesson.id}', ${index})">
<span class="lesson-number">${lesson.is_completed ? '✓' : index + 1}</span>
<div class="lesson-info">
<h5>${escapeHtml(lesson.title)}</h5>
<span>${formatDuration(lesson.duration_minutes)}</span>
</div>
<span class="lesson-action">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
</span>
</div>
`).join('');
} else {
lessonsList.innerHTML = '<p class="text-secondary">Nenhuma aula disponível.</p>';
}
// Render quiz section
const quizSection = document.getElementById('modalQuizSection');
if (quiz) {
quizSection.style.display = 'block';
const questions = typeof quiz.questions === 'string'
? JSON.parse(quiz.questions)
: (quiz.questions || []);
document.getElementById('modalQuizQuestions').textContent = `${questions.length} questões`;
document.getElementById('modalQuizTime').textContent = quiz.time_limit_minutes
? `${quiz.time_limit_minutes} min`
: 'Sem limite';
document.getElementById('modalQuizPassing').textContent = `${quiz.passing_score}% para aprovação`;
LearnState.currentQuiz = quiz;
} else {
quizSection.style.display = 'none';
}
// Update button text based on progress
const startBtn = document.getElementById('startCourseBtn');
if (data.user_progress) {
const progress = data.user_progress;
if (progress.status === 'completed') {
startBtn.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="1 4 1 10 7 10"></polyline>
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"></path>
</svg>
<span>Revisar Curso</span>
`;
} else if (progress.status === 'in_progress') {
startBtn.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
<span>Continuar</span>
`;
}
// Show progress
document.getElementById('modalProgress').style.display = 'block';
document.getElementById('modalProgressFill').style.width = `${progress.completion_percentage || 0}%`;
document.getElementById('modalProgressText').textContent = `${progress.completion_percentage || 0}% completo`;
} else {
document.getElementById('modalProgress').style.display = 'none';
startBtn.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
<span>Iniciar Curso</span>
`;
}
}
function startCourse() {
if (!LearnState.currentCourse) return;
const course = LearnState.currentCourse.course || LearnState.currentCourse;
const lessons = LearnState.currentCourse.lessons || [];
// Start course via API
startCourseAPI(course.id);
// Open first lesson
if (lessons.length > 0) {
openLesson(lessons[0].id, 0);
} else {
showNotification('info', 'Este curso ainda não possui aulas.');
}
}
function openLesson(lessonId, index) {
const lessons = LearnState.currentCourse?.lessons || [];
const lesson = lessons.find(l => l.id === lessonId) || lessons[index];
if (!lesson) {
showNotification('error', 'Aula não encontrada.');
return;
}
LearnState.currentLesson = lesson;
LearnState.currentLessonIndex = index;
// Close course modal, open lesson modal
closeCourseModal();
document.getElementById('lessonModal').classList.remove('hidden');
// Update lesson UI
document.getElementById('lessonTitle').textContent = lesson.title;
document.getElementById('lessonNavTitle').textContent = `Aula ${index + 1} de ${lessons.length}`;
// Render content based on type
const contentDiv = document.getElementById('lessonContent');
if (lesson.video_url) {
contentDiv.innerHTML = `
<div class="video-container">
<iframe src="${lesson.video_url}" frameborder="0" allowfullscreen></iframe>
</div>
<div class="lesson-text">
${lesson.content || ''}
</div>
`;
} else {
contentDiv.innerHTML = `
<div class="lesson-text">
${lesson.content || '<p>Conteúdo da aula será exibido aqui.</p>'}
</div>
`;
}
// Update sidebar list
const sidebar = document.getElementById('lessonListSidebar');
sidebar.innerHTML = lessons.map((l, i) => `
<div class="lesson-item ${l.is_completed ? 'completed' : ''} ${l.id === lesson.id ? 'active' : ''}"
onclick="openLesson('${l.id}', ${i})">
<span class="lesson-number">${l.is_completed ? '✓' : i + 1}</span>
<div class="lesson-info">
<h5>${escapeHtml(l.title)}</h5>
</div>
</div>
`).join('');
// Update navigation buttons
document.getElementById('prevLessonBtn').disabled = index === 0;
document.getElementById('nextLessonBtn').disabled = index >= lessons.length - 1;
// Update progress
const progress = ((index + 1) / lessons.length) * 100;
document.getElementById('lessonProgressFill').style.width = `${progress}%`;
}
function closeLessonModal() {
document.getElementById('lessonModal').classList.add('hidden');
LearnState.currentLesson = null;
// Reopen course modal

600
ui/suite/social/social.css Normal file
View file

@ -0,0 +1,600 @@
.social-app {
display: flex;
flex-direction: column;
height: 100%;
background: var(--bg);
}
.social-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
background: var(--surface);
border-bottom: 1px solid var(--border);
}
.social-tabs {
display: flex;
gap: 0.25rem;
}
.social-tab {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.social-tab:hover {
background: var(--surface-hover);
color: var(--text);
}
.social-tab.active {
background: var(--primary);
color: white;
}
.social-tab svg {
width: 18px;
height: 18px;
}
.btn-new-post {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: var(--primary);
border: none;
border-radius: 6px;
color: white;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-new-post:hover {
background: var(--primary-hover);
}
.social-layout {
display: grid;
grid-template-columns: 260px 1fr 280px;
gap: 1rem;
flex: 1;
padding: 1rem;
overflow: hidden;
}
.social-sidebar,
.social-right-sidebar {
display: flex;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
}
.sidebar-section {
background: var(--surface);
border-radius: 8px;
padding: 1rem;
border: 1px solid var(--border);
}
.sidebar-section h3 {
font-size: 0.875rem;
font-weight: 600;
color: var(--text);
margin: 0 0 0.75rem 0;
}
.social-main {
display: flex;
flex-direction: column;
gap: 1rem;
overflow-y: auto;
}
.post-composer {
display: flex;
align-items: flex-start;
gap: 0.75rem;
padding: 1rem;
background: var(--surface);
border-radius: 8px;
border: 1px solid var(--border);
}
.composer-avatar .avatar-placeholder {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--primary);
}
.composer-input {
flex: 1;
padding: 0.75rem 1rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 20px;
color: var(--text-secondary);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.15s ease;
}
.composer-input:hover {
border-color: var(--primary);
background: var(--surface-hover);
}
.composer-actions {
display: flex;
gap: 0.25rem;
}
.composer-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: transparent;
border: none;
border-radius: 50%;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s ease;
}
.composer-btn:hover {
background: var(--surface-hover);
color: var(--primary);
}
.post-card {
background: var(--surface);
border-radius: 8px;
border: 1px solid var(--border);
overflow: hidden;
}
.post-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
}
.post-header .avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
}
.post-meta {
display: flex;
flex-direction: column;
}
.author-name {
font-weight: 600;
color: var(--text);
font-size: 0.875rem;
}
.post-time {
font-size: 0.75rem;
color: var(--text-secondary);
}
.post-content {
padding: 0 1rem 1rem;
color: var(--text);
font-size: 0.9375rem;
line-height: 1.5;
}
.post-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-top: 1px solid var(--border);
}
.reactions {
display: flex;
gap: 0.5rem;
}
.reaction {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.25rem 0.5rem;
background: var(--bg);
border-radius: 12px;
font-size: 0.75rem;
color: var(--text-secondary);
}
.post-actions {
display: flex;
gap: 0.25rem;
}
.post-actions button {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0.75rem;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
font-size: 0.8125rem;
cursor: pointer;
transition: all 0.15s ease;
}
.post-actions button:hover {
background: var(--surface-hover);
color: var(--primary);
}
.comments-section {
border-top: 1px solid var(--border);
padding: 1rem;
}
.empty-feed {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
text-align: center;
color: var(--text-secondary);
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem;
gap: 1rem;
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.loading-placeholder {
height: 60px;
background: linear-gradient(90deg, var(--bg) 25%, var(--surface-hover) 50%, var(--bg) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: 6px;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.modal {
display: none;
position: fixed;
inset: 0;
z-index: 1000;
}
.modal.active {
display: flex;
align-items: center;
justify-content: center;
}
.modal-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
}
.modal-content {
position: relative;
width: 100%;
max-width: 560px;
background: var(--surface);
border-radius: 12px;
border: 1px solid var(--border);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-bottom: 1px solid var(--border);
}
.modal-header h3 {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--text);
}
.btn-close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
font-size: 1.5rem;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-close:hover {
background: var(--surface-hover);
color: var(--text);
}
.modal-body {
padding: 1rem;
}
.post-author-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 1rem;
}
.post-author-row .avatar-placeholder {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--primary);
}
.author-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.visibility-select {
padding: 0.25rem 0.5rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-secondary);
font-size: 0.75rem;
cursor: pointer;
}
.post-textarea {
width: 100%;
min-height: 120px;
padding: 0.75rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text);
font-size: 0.9375rem;
font-family: inherit;
resize: vertical;
transition: border-color 0.15s ease;
}
.post-textarea:focus {
outline: none;
border-color: var(--primary);
}
.post-textarea::placeholder {
color: var(--text-secondary);
}
.post-attachments {
margin-top: 0.75rem;
}
.poll-creator {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 0.75rem;
}
.poll-question {
width: 100%;
padding: 0.5rem;
background: transparent;
border: none;
border-bottom: 1px solid var(--border);
color: var(--text);
font-size: 0.9375rem;
margin-bottom: 0.75rem;
}
.poll-question:focus {
outline: none;
border-color: var(--primary);
}
.poll-options {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.poll-options input {
padding: 0.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 0.875rem;
}
.poll-options input:focus {
outline: none;
border-color: var(--primary);
}
.btn-add-option {
padding: 0.5rem;
background: transparent;
border: 1px dashed var(--border);
border-radius: 6px;
color: var(--text-secondary);
font-size: 0.8125rem;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-add-option:hover {
border-color: var(--primary);
color: var(--primary);
}
.post-options {
margin-top: 0.75rem;
}
.community-select {
width: 100%;
padding: 0.5rem;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
font-size: 0.875rem;
cursor: pointer;
}
.modal-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 1rem;
border-top: 1px solid var(--border);
}
.post-tools {
display: flex;
gap: 0.25rem;
}
.tool-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
background: transparent;
border: none;
border-radius: 6px;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.15s ease;
}
.tool-btn:hover {
background: var(--surface-hover);
color: var(--primary);
}
.btn-post {
padding: 0.5rem 1.5rem;
background: var(--primary);
border: none;
border-radius: 6px;
color: white;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-post:hover {
background: var(--primary-hover);
}
.btn-post:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (max-width: 1200px) {
.social-layout {
grid-template-columns: 220px 1fr;
}
.social-right-sidebar {
display: none;
}
}
@media (max-width: 900px) {
.social-layout {
grid-template-columns: 1fr;
}
.social-sidebar {
display: none;
}
}
@media (max-width: 600px) {
.social-header {
flex-direction: column;
gap: 0.75rem;
}
.social-tabs {
width: 100%;
overflow-x: auto;
}
.social-tab span {
display: none;
}
.btn-new-post span {
display: none;
}
.modal-content {
margin: 1rem;
max-height: calc(100vh - 2rem);
overflow-y: auto;
}
}

244
ui/suite/social/social.html Normal file
View file

@ -0,0 +1,244 @@
<link rel="stylesheet" href="social/social.css" />
<div class="social-app">
<div class="social-header">
<div class="social-tabs">
<button class="social-tab active" data-tab="feed" hx-get="/api/ui/social/feed" hx-target="#social-content" hx-swap="innerHTML">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>
<span data-i18n="social-feed">Feed</span>
</button>
<button class="social-tab" data-tab="communities" hx-get="/api/ui/social/communities" hx-target="#social-content" hx-swap="innerHTML">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span data-i18n="social-communities">Communities</span>
</button>
<button class="social-tab" data-tab="announcements" hx-get="/api/ui/social/announcements" hx-target="#social-content" hx-swap="innerHTML">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 17H2a3 3 0 0 0 3-3V9a7 7 0 0 1 14 0v5a3 3 0 0 0 3 3zm-8.27 4a2 2 0 0 1-3.46 0"/>
</svg>
<span data-i18n="social-announcements">Announcements</span>
</button>
</div>
<div class="social-actions">
<button class="btn-new-post" onclick="showNewPostModal()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
<span data-i18n="social-new-post">New Post</span>
</button>
</div>
</div>
<div class="social-layout">
<aside class="social-sidebar">
<div class="sidebar-section">
<h3 data-i18n="social-my-communities">My Communities</h3>
<div id="my-communities" hx-get="/api/ui/social/my-communities" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-placeholder"></div>
</div>
</div>
<div class="sidebar-section">
<h3 data-i18n="social-trending">Trending</h3>
<div id="trending-topics" hx-get="/api/ui/social/trending" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-placeholder"></div>
</div>
</div>
<div class="sidebar-section">
<h3 data-i18n="social-suggested">Suggested Communities</h3>
<div id="suggested-communities" hx-get="/api/ui/social/suggested" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-placeholder"></div>
</div>
</div>
</aside>
<main class="social-main">
<div class="post-composer">
<div class="composer-avatar">
<div class="avatar-placeholder"></div>
</div>
<div class="composer-input" onclick="showNewPostModal()">
<span data-i18n="social-whats-on-mind">What's on your mind?</span>
</div>
<div class="composer-actions">
<button class="composer-btn" title="Add image">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
</button>
<button class="composer-btn" title="Add poll">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
</button>
<button class="composer-btn" title="Send praise">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</button>
</div>
</div>
<div id="social-content" hx-get="/api/ui/social/feed" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state">
<div class="spinner"></div>
<span data-i18n="loading">Loading...</span>
</div>
</div>
</main>
<aside class="social-right-sidebar">
<div class="sidebar-section">
<h3 data-i18n="social-recent-activity">Recent Activity</h3>
<div id="recent-activity" hx-get="/api/ui/social/activity" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-placeholder"></div>
</div>
</div>
<div class="sidebar-section">
<h3 data-i18n="social-people">People to Follow</h3>
<div id="people-suggestions" hx-get="/api/ui/social/people" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-placeholder"></div>
</div>
</div>
</aside>
</div>
</div>
<div class="modal" id="newPostModal">
<div class="modal-backdrop" onclick="closeNewPostModal()"></div>
<div class="modal-content">
<div class="modal-header">
<h3 data-i18n="social-create-post">Create Post</h3>
<button class="btn-close" onclick="closeNewPostModal()">&times;</button>
</div>
<form id="newPostForm" hx-post="/api/social/posts" hx-swap="none" hx-on::after-request="handlePostCreated(event)">
<div class="modal-body">
<div class="post-author-row">
<div class="avatar-placeholder"></div>
<div class="author-info">
<span class="author-name" id="currentUserName">User</span>
<select name="visibility" class="visibility-select">
<option value="organization" data-i18n="social-org-only">Organization</option>
<option value="community" data-i18n="social-community-only">Community Only</option>
<option value="public" data-i18n="social-public">Public</option>
</select>
</div>
</div>
<textarea name="content" class="post-textarea" placeholder="What's on your mind?" rows="5" required></textarea>
<div class="post-attachments" id="postAttachments"></div>
<div class="post-options">
<select name="community_id" class="community-select">
<option value="" data-i18n="social-no-community">No Community</option>
</select>
</div>
</div>
<div class="modal-footer">
<div class="post-tools">
<button type="button" class="tool-btn" title="Add image">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
<circle cx="8.5" cy="8.5" r="1.5"/>
<polyline points="21 15 16 10 5 21"/>
</svg>
</button>
<button type="button" class="tool-btn" title="Add poll" onclick="togglePollCreator()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="20" x2="18" y2="10"/>
<line x1="12" y1="20" x2="12" y2="4"/>
<line x1="6" y1="20" x2="6" y2="14"/>
</svg>
</button>
<button type="button" class="tool-btn" title="Mention someone">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="4"/>
<path d="M16 8v5a3 3 0 0 0 6 0v-1a10 10 0 1 0-3.92 7.94"/>
</svg>
</button>
</div>
<button type="submit" class="btn-post">
<span data-i18n="social-post">Post</span>
</button>
</div>
</form>
</div>
</div>
<script>
(function() {
"use strict";
function showNewPostModal() {
document.getElementById("newPostModal").classList.add("active");
document.querySelector(".post-textarea").focus();
}
function closeNewPostModal() {
document.getElementById("newPostModal").classList.remove("active");
document.getElementById("newPostForm").reset();
}
function handlePostCreated(event) {
if (event.detail.successful) {
closeNewPostModal();
htmx.trigger("#social-content", "refresh");
if (window.GBAlerts) {
window.GBAlerts.info("Social", "Post created successfully!");
}
}
}
function togglePollCreator() {
var attachments = document.getElementById("postAttachments");
var existing = attachments.querySelector(".poll-creator");
if (existing) {
existing.remove();
return;
}
var pollHtml = '<div class="poll-creator">' +
'<input type="text" name="poll_question" placeholder="Ask a question..." class="poll-question" />' +
'<div class="poll-options">' +
'<input type="text" name="poll_option_1" placeholder="Option 1" />' +
'<input type="text" name="poll_option_2" placeholder="Option 2" />' +
'<button type="button" class="btn-add-option" onclick="addPollOption()">+ Add option</button>' +
'</div>' +
'</div>';
attachments.innerHTML = pollHtml;
}
function addPollOption() {
var options = document.querySelector(".poll-options");
var count = options.querySelectorAll("input").length + 1;
var input = document.createElement("input");
input.type = "text";
input.name = "poll_option_" + count;
input.placeholder = "Option " + count;
options.insertBefore(input, options.querySelector(".btn-add-option"));
}
document.querySelectorAll(".social-tab").forEach(function(tab) {
tab.addEventListener("click", function() {
document.querySelectorAll(".social-tab").forEach(function(t) {
t.classList.remove("active");
});
this.classList.add("active");
});
});
window.showNewPostModal = showNewPostModal;
window.closeNewPostModal = closeNewPostModal;
window.handlePostCreated = handlePostCreated;
window.togglePollCreator = togglePollCreator;
window.addPollOption = addPollOption;
})();
</script>

1347
ui/suite/video/video.html Normal file

File diff suppressed because it is too large Load diff

1802
ui/suite/video/video.js Normal file

File diff suppressed because it is too large Load diff