Add admin, dashboards, learn, social, and video UI components
This commit is contained in:
parent
5f65a62808
commit
cb33a75d39
24 changed files with 17830 additions and 1 deletions
432
ui/suite/admin/accounts.html
Normal file
432
ui/suite/admin/accounts.html
Normal 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">
|
||||
558
ui/suite/admin/admin-dashboard.html
Normal file
558
ui/suite/admin/admin-dashboard.html
Normal 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>
|
||||
467
ui/suite/admin/billing-dashboard.html
Normal file
467
ui/suite/admin/billing-dashboard.html
Normal 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
777
ui/suite/admin/billing.html
Normal 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;
|
||||
455
ui/suite/admin/compliance-dashboard.html
Normal file
455
ui/suite/admin/compliance-dashboard.html
Normal 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
1116
ui/suite/admin/contacts.html
Normal file
File diff suppressed because it is too large
Load diff
555
ui/suite/admin/onboarding.html
Normal file
555
ui/suite/admin/onboarding.html
Normal 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.
|
||||
499
ui/suite/admin/operations-dashboard.html
Normal file
499
ui/suite/admin/operations-dashboard.html
Normal 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
|
||||
868
ui/suite/admin/organization-settings.html
Normal file
868
ui/suite/admin/organization-settings.html
Normal 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
|
||||
822
ui/suite/admin/organization-switcher.html
Normal file
822
ui/suite/admin/organization-switcher.html
Normal 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">×</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, '"')})">
|
||||
<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
655
ui/suite/admin/roles.html
Normal 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:
|
||||
1324
ui/suite/admin/search-settings.html
Normal file
1324
ui/suite/admin/search-settings.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -289,6 +289,52 @@
|
|||
<span>Analytics</span>
|
||||
</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
|
||||
href="#monitoring"
|
||||
class="app-item"
|
||||
|
|
|
|||
933
ui/suite/dashboards/dashboards.css
Normal file
933
ui/suite/dashboards/dashboards.css
Normal 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;
|
||||
}
|
||||
}
|
||||
1437
ui/suite/dashboards/dashboards.html
Normal file
1437
ui/suite/dashboards/dashboards.html
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -31,6 +31,7 @@
|
|||
<link rel="stylesheet" href="tasks/tasks.css?v=20251230" />
|
||||
<link rel="stylesheet" href="tasks/taskmd.css?v=20251230" />
|
||||
<link rel="stylesheet" href="analytics/analytics.css" />
|
||||
<link rel="stylesheet" href="dashboards/dashboards.css" />
|
||||
<link rel="stylesheet" href="monitoring/monitoring.css" />
|
||||
|
||||
<!-- Local Libraries (no external CDN dependencies) -->
|
||||
|
|
@ -351,6 +352,31 @@
|
|||
</svg>
|
||||
<span data-i18n="nav-slides">Apresentações</span>
|
||||
</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>
|
||||
|
||||
<!-- Apps menu container (button + dropdown) -->
|
||||
|
|
@ -771,6 +797,92 @@
|
|||
<span data-i18n="nav-analytics">Analytics</span>
|
||||
</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 -->
|
||||
<a
|
||||
class="app-item"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"nav-meet": "Meet",
|
||||
"nav-research": "Research",
|
||||
"nav-analytics": "Analytics",
|
||||
"nav-dashboards": "Dashboards",
|
||||
"nav-monitoring": "Monitoring",
|
||||
"nav-admin": "Admin",
|
||||
"nav-sources": "Sources",
|
||||
|
|
@ -347,6 +348,77 @@
|
|||
"compliance-filter-severity": "Severity:",
|
||||
"compliance-filter-type": "Type:",
|
||||
"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-subtitle": "Manage data sources, prompts, and integrations",
|
||||
"sources-search": "Search sources...",
|
||||
|
|
@ -404,7 +476,8 @@
|
|||
"nav-tasks": "Tarefas",
|
||||
"nav-meet": "Reuniões",
|
||||
"nav-research": "Pesquisa",
|
||||
"nav-analytics": "Análises",
|
||||
"nav-analytics": "Analytics",
|
||||
"nav-dashboards": "Painéis",
|
||||
"nav-monitoring": "Monitoramento",
|
||||
"nav-admin": "Administração",
|
||||
"nav-sources": "Fontes",
|
||||
|
|
@ -735,6 +808,77 @@
|
|||
"compliance-filter-severity": "Severidade:",
|
||||
"compliance-filter-type": "Tipo:",
|
||||
"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-subtitle": "Gerenciar fontes de dados, prompts e integrações",
|
||||
"sources-search": "Buscar fontes...",
|
||||
|
|
|
|||
1249
ui/suite/learn/learn.css
Normal file
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
563
ui/suite/learn/learn.html
Normal 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
824
ui/suite/learn/learn.js
Normal 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
600
ui/suite/social/social.css
Normal 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
244
ui/suite/social/social.html
Normal 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()">×</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
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
1802
ui/suite/video/video.js
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue