diff --git a/ui/suite/admin/admin-functions.js b/ui/suite/admin/admin-functions.js index 4dfaef4..1eb98eb 100644 --- a/ui/suite/admin/admin-functions.js +++ b/ui/suite/admin/admin-functions.js @@ -3,724 +3,1561 @@ These functions are called by onclick handlers in admin HTML files ============================================================================= */ -(function() { - 'use strict'; +(function () { + "use strict"; - // ============================================================================= - // MODAL HELPERS - // ============================================================================= + // ============================================================================= + // MODAL HELPERS + // ============================================================================= - function showModal(modalId) { - const modal = document.getElementById(modalId); - if (modal) { - if (modal.showModal) { - modal.showModal(); - } else { - modal.classList.add('open'); - modal.style.display = 'flex'; - } - } + function showModal(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + if (modal.showModal) { + modal.showModal(); + } else { + modal.classList.add("open"); + modal.style.display = "flex"; + } + } + } + + function hideModal(modalId) { + const modal = document.getElementById(modalId); + if (modal) { + if (modal.close) { + modal.close(); + } else { + modal.classList.remove("open"); + modal.style.display = "none"; + } + } + } + + function showNotification(message, type) { + if (typeof window.showNotification === "function") { + window.showNotification(message, type); + } else if (typeof window.GBAlerts !== "undefined") { + if (type === "success") window.GBAlerts.success("Admin", message); + else if (type === "error") window.GBAlerts.error("Admin", message); + else if (type === "warning") window.GBAlerts.warning("Admin", message); + else window.GBAlerts.info("Admin", message); + } else { + console.log(`[${type}] ${message}`); + } + } + + // ============================================================================= + // ACCOUNTS.HTML FUNCTIONS + // ============================================================================= + + function showSmtpModal() { + showModal("smtp-modal"); + } + + function closeSmtpModal() { + hideModal("smtp-modal"); + } + + function testSmtpConnection() { + const host = document.getElementById("smtp-host")?.value; + const port = document.getElementById("smtp-port")?.value; + const username = document.getElementById("smtp-username")?.value; + + if (!host || !port) { + showNotification("Please fill in SMTP host and port", "error"); + return; } - function hideModal(modalId) { - const modal = document.getElementById(modalId); - if (modal) { - if (modal.close) { - modal.close(); - } else { - modal.classList.remove('open'); - modal.style.display = 'none'; - } - } - } + showNotification("Testing SMTP connection...", "info"); - function showNotification(message, type) { - if (typeof window.showNotification === 'function') { - window.showNotification(message, type); - } else if (typeof window.GBAlerts !== 'undefined') { - if (type === 'success') window.GBAlerts.success('Admin', message); - else if (type === 'error') window.GBAlerts.error('Admin', message); - else if (type === 'warning') window.GBAlerts.warning('Admin', message); - else window.GBAlerts.info('Admin', message); + fetch("/api/settings/smtp/test", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ host, port: parseInt(port), username }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("SMTP connection successful!", "success"); } else { - console.log(`[${type}] ${message}`); + showNotification( + "SMTP connection failed: " + (data.error || "Unknown error"), + "error", + ); } - } + }) + .catch((err) => { + showNotification("Connection test failed: " + err.message, "error"); + }); + } - // ============================================================================= - // ACCOUNTS.HTML FUNCTIONS - // ============================================================================= + function connectAccount(provider) { + showNotification(`Connecting to ${provider}...`, "info"); + // OAuth flow would redirect to provider + window.location.href = `/api/auth/oauth/${provider}?redirect=/admin/accounts`; + } - function showSmtpModal() { - showModal('smtp-modal'); - } + function disconnectAccount(provider) { + if (!confirm(`Disconnect ${provider} account?`)) return; - function closeSmtpModal() { - hideModal('smtp-modal'); - } - - function testSmtpConnection() { - const host = document.getElementById('smtp-host')?.value; - const port = document.getElementById('smtp-port')?.value; - const username = document.getElementById('smtp-username')?.value; - - if (!host || !port) { - showNotification('Please fill in SMTP host and port', 'error'); - return; - } - - showNotification('Testing SMTP connection...', 'info'); - - fetch('/api/settings/smtp/test', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ host, port: parseInt(port), username }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('SMTP connection successful!', 'success'); - } else { - showNotification('SMTP connection failed: ' + (data.error || 'Unknown error'), 'error'); - } - }) - .catch(err => { - showNotification('Connection test failed: ' + err.message, 'error'); - }); - } - - function connectAccount(provider) { - showNotification(`Connecting to ${provider}...`, 'info'); - // OAuth flow would redirect to provider - window.location.href = `/api/auth/oauth/${provider}?redirect=/admin/accounts`; - } - - function disconnectAccount(provider) { - if (!confirm(`Disconnect ${provider} account?`)) return; - - fetch(`/api/settings/accounts/${provider}/disconnect`, { method: 'POST' }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification(`${provider} disconnected`, 'success'); - location.reload(); - } else { - showNotification('Failed to disconnect: ' + data.error, 'error'); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); - } - - // ============================================================================= - // ADMIN-DASHBOARD.HTML FUNCTIONS - // ============================================================================= - - function showInviteMemberModal() { - showModal('invite-member-modal'); - } - - function closeInviteMemberModal() { - hideModal('invite-member-modal'); - } - - function showBulkInviteModal() { - showModal('bulk-invite-modal'); - } - - function closeBulkInviteModal() { - hideModal('bulk-invite-modal'); - } - - function sendInvitation() { - const email = document.getElementById('invite-email')?.value; - const role = document.getElementById('invite-role')?.value || 'member'; - - if (!email) { - showNotification('Please enter an email address', 'error'); - return; - } - - fetch('/api/admin/invitations', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, role }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('Invitation sent to ' + email, 'success'); - closeInviteMemberModal(); - } else { - showNotification('Failed to send invitation: ' + data.error, 'error'); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); - } - - function sendBulkInvitations() { - const emailsText = document.getElementById('bulk-emails')?.value || ''; - const role = document.getElementById('bulk-role')?.value || 'member'; - const emails = emailsText.split(/[\n,;]+/).map(e => e.trim()).filter(e => e); - - if (emails.length === 0) { - showNotification('Please enter at least one email address', 'error'); - return; - } - - fetch('/api/admin/invitations/bulk', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ emails, role }) - }) - .then(response => response.json()) - .then(data => { - showNotification(`${data.sent || emails.length} invitations sent`, 'success'); - closeBulkInviteModal(); - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); - } - - function resendInvitation(invitationId) { - fetch(`/api/admin/invitations/${invitationId}/resend`, { method: 'POST' }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('Invitation resent', 'success'); - } else { - showNotification('Failed to resend: ' + data.error, 'error'); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); - } - - function cancelInvitation(invitationId) { - if (!confirm('Cancel this invitation?')) return; - - fetch(`/api/admin/invitations/${invitationId}`, { method: 'DELETE' }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('Invitation cancelled', 'success'); - location.reload(); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); - } - - // ============================================================================= - // BILLING-DASHBOARD.HTML FUNCTIONS - // ============================================================================= - - function updateBillingPeriod(period) { - const params = new URLSearchParams({ period }); - - // Update dashboard stats via HTMX or fetch - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', `/api/admin/billing/stats?${params}`, '#billing-stats'); + fetch(`/api/settings/accounts/${provider}/disconnect`, { method: "POST" }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification(`${provider} disconnected`, "success"); + location.reload(); } else { - fetch(`/api/admin/billing/stats?${params}`) - .then(r => r.json()) - .then(data => updateBillingStats(data)) - .catch(err => console.error('Failed to update billing period:', err)); + showNotification("Failed to disconnect: " + data.error, "error"); } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } + + // ============================================================================= + // ADMIN-DASHBOARD.HTML FUNCTIONS + // ============================================================================= + + function showInviteMemberModal() { + showModal("invite-member-modal"); + } + + function closeInviteMemberModal() { + hideModal("invite-member-modal"); + } + + function showBulkInviteModal() { + showModal("bulk-invite-modal"); + } + + function closeBulkInviteModal() { + hideModal("bulk-invite-modal"); + } + + function sendInvitation() { + const email = document.getElementById("invite-email")?.value; + const role = document.getElementById("invite-role")?.value || "member"; + + if (!email) { + showNotification("Please enter an email address", "error"); + return; } - function updateBillingStats(data) { - if (data.totalRevenue) { - const el = document.getElementById('total-revenue'); - if (el) el.textContent = formatCurrency(data.totalRevenue); - } - if (data.activeSubscriptions) { - const el = document.getElementById('active-subscriptions'); - if (el) el.textContent = data.activeSubscriptions; - } - } - - function exportBillingReport() { - const period = document.getElementById('billingPeriod')?.value || 'current'; - showNotification('Generating billing report...', 'info'); - - fetch(`/api/admin/billing/export?period=${period}`) - .then(response => { - if (response.ok) return response.blob(); - throw new Error('Export failed'); - }) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `billing-report-${period}.csv`; - a.click(); - URL.revokeObjectURL(url); - showNotification('Report downloaded', 'success'); - }) - .catch(err => showNotification('Export failed: ' + err.message, 'error')); - } - - function toggleBreakdownView() { - const chart = document.getElementById('breakdown-chart'); - const table = document.getElementById('breakdown-table'); - - if (chart && table) { - const showingChart = !chart.classList.contains('hidden'); - chart.classList.toggle('hidden', showingChart); - table.classList.toggle('hidden', !showingChart); - } - } - - function showQuotaSettings() { - showModal('quota-settings-modal'); - } - - function closeQuotaSettings() { - hideModal('quota-settings-modal'); - } - - function saveQuotaSettings() { - const form = document.getElementById('quota-form'); - if (!form) return; - - const formData = new FormData(form); - const quotas = Object.fromEntries(formData); - - fetch('/api/admin/billing/quotas', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(quotas) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('Quota settings saved', 'success'); - closeQuotaSettings(); - } else { - showNotification('Failed to save: ' + data.error, 'error'); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); - } - - function configureAlerts() { - showModal('alerts-config-modal'); - } - - function closeAlertsConfig() { - hideModal('alerts-config-modal'); - } - - function saveAlertSettings() { - const form = document.getElementById('alerts-form'); - if (!form) return; - - const formData = new FormData(form); - const settings = Object.fromEntries(formData); - - fetch('/api/admin/billing/alerts', { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(settings) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('Alert settings saved', 'success'); - closeAlertsConfig(); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); - } - - // ============================================================================= - // BILLING.HTML FUNCTIONS - // ============================================================================= - - function showUpgradeModal() { - showModal('upgrade-modal'); - } - - function closeUpgradeModal() { - hideModal('upgrade-modal'); - } - - function showCancelModal() { - showModal('cancel-modal'); - } - - function closeCancelModal() { - hideModal('cancel-modal'); - } - - function showAddPaymentModal() { - showModal('add-payment-modal'); - } - - function closeAddPaymentModal() { - hideModal('add-payment-modal'); - } - - function showEditAddressModal() { - showModal('edit-address-modal'); - } - - function closeEditAddressModal() { - hideModal('edit-address-modal'); - } - - function exportInvoices() { - showNotification('Exporting invoices...', 'info'); - - fetch('/api/billing/invoices/export') - .then(response => { - if (response.ok) return response.blob(); - throw new Error('Export failed'); - }) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'invoices.csv'; - a.click(); - URL.revokeObjectURL(url); - showNotification('Invoices exported', 'success'); - }) - .catch(err => showNotification('Export failed: ' + err.message, 'error')); - } - - function contactSales() { - window.open('mailto:sales@example.com?subject=Enterprise Plan Inquiry', '_blank'); - } - - function showDowngradeOptions() { - closeCancelModal(); - showUpgradeModal(); - // Focus on lower-tier plans - const planSelector = document.querySelector('.plan-options'); - if (planSelector) { - planSelector.scrollIntoView({ behavior: 'smooth' }); - } - } - - function selectPlan(planId) { - document.querySelectorAll('.plan-option').forEach(el => { - el.classList.toggle('selected', el.dataset.plan === planId); - }); - } - - function confirmUpgrade() { - const selectedPlan = document.querySelector('.plan-option.selected'); - if (!selectedPlan) { - showNotification('Please select a plan', 'error'); - return; - } - - const planId = selectedPlan.dataset.plan; - - fetch('/api/billing/subscription/upgrade', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ plan_id: planId }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('Plan upgraded successfully!', 'success'); - closeUpgradeModal(); - location.reload(); - } else { - showNotification('Upgrade failed: ' + data.error, 'error'); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); - } - - function confirmCancellation() { - const reason = document.getElementById('cancel-reason')?.value; - - fetch('/api/billing/subscription/cancel', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ reason }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('Subscription cancelled', 'success'); - closeCancelModal(); - location.reload(); - } else { - showNotification('Cancellation failed: ' + data.error, 'error'); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); - } - - // ============================================================================= - // COMPLIANCE-DASHBOARD.HTML FUNCTIONS - // ============================================================================= - - function updateFramework(framework) { - // Update dashboard for selected compliance framework - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', `/api/compliance/dashboard?framework=${framework}`, '#compliance-content'); + fetch("/api/admin/invitations", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, role }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Invitation sent to " + email, "success"); + closeInviteMemberModal(); } else { - fetch(`/api/compliance/dashboard?framework=${framework}`) - .then(r => r.json()) - .then(data => updateComplianceDashboard(data)) - .catch(err => console.error('Failed to update framework:', err)); + showNotification("Failed to send invitation: " + data.error, "error"); } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } + + function sendBulkInvitations() { + const emailsText = document.getElementById("bulk-emails")?.value || ""; + const role = document.getElementById("bulk-role")?.value || "member"; + const emails = emailsText + .split(/[\n,;]+/) + .map((e) => e.trim()) + .filter((e) => e); + + if (emails.length === 0) { + showNotification("Please enter at least one email address", "error"); + return; } - function updateComplianceDashboard(data) { - // Update various dashboard elements - if (data.score) { - const el = document.getElementById('compliance-score'); - if (el) el.textContent = data.score + '%'; + fetch("/api/admin/invitations/bulk", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ emails, role }), + }) + .then((response) => response.json()) + .then((data) => { + showNotification( + `${data.sent || emails.length} invitations sent`, + "success", + ); + closeBulkInviteModal(); + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } + + function resendInvitation(invitationId) { + fetch(`/api/admin/invitations/${invitationId}/resend`, { method: "POST" }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Invitation resent", "success"); + } else { + showNotification("Failed to resend: " + data.error, "error"); } - } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } - function generateComplianceReport() { - const framework = document.getElementById('complianceFramework')?.value || 'soc2'; - showNotification('Generating compliance report...', 'info'); + function cancelInvitation(invitationId) { + if (!confirm("Cancel this invitation?")) return; - fetch(`/api/compliance/report?framework=${framework}`) - .then(response => { - if (response.ok) return response.blob(); - throw new Error('Report generation failed'); - }) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `compliance-report-${framework}.pdf`; - a.click(); - URL.revokeObjectURL(url); - showNotification('Report generated', 'success'); - }) - .catch(err => showNotification('Report failed: ' + err.message, 'error')); - } - - function startAuditPrep() { - showModal('audit-prep-modal'); - } - - function closeAuditPrep() { - hideModal('audit-prep-modal'); - } - - function showEvidenceUpload() { - showModal('evidence-upload-modal'); - } - - function closeEvidenceUpload() { - hideModal('evidence-upload-modal'); - } - - function uploadEvidence() { - const fileInput = document.getElementById('evidence-file'); - const category = document.getElementById('evidence-category')?.value; - - if (!fileInput?.files?.length) { - showNotification('Please select a file', 'error'); - return; + fetch(`/api/admin/invitations/${invitationId}`, { method: "DELETE" }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Invitation cancelled", "success"); + location.reload(); } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } - const formData = new FormData(); - formData.append('file', fileInput.files[0]); - formData.append('category', category); + // ============================================================================= + // BILLING-DASHBOARD.HTML FUNCTIONS + // ============================================================================= - fetch('/api/compliance/evidence', { - method: 'POST', - body: formData - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('Evidence uploaded', 'success'); - closeEvidenceUpload(); - } else { - showNotification('Upload failed: ' + data.error, 'error'); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); + function updateBillingPeriod(period) { + const params = new URLSearchParams({ period }); + + // Update dashboard stats via HTMX or fetch + if (typeof htmx !== "undefined") { + htmx.ajax("GET", `/api/admin/billing/stats?${params}`, "#billing-stats"); + } else { + fetch(`/api/admin/billing/stats?${params}`) + .then((r) => r.json()) + .then((data) => updateBillingStats(data)) + .catch((err) => console.error("Failed to update billing period:", err)); } + } - function filterLogs() { - const category = document.getElementById('logCategory')?.value || 'all'; + function updateBillingStats(data) { + if (data.totalRevenue) { + const el = document.getElementById("total-revenue"); + if (el) el.textContent = formatCurrency(data.totalRevenue); + } + if (data.activeSubscriptions) { + const el = document.getElementById("active-subscriptions"); + if (el) el.textContent = data.activeSubscriptions; + } + } - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', `/api/compliance/audit-log?category=${category}`, '#audit-log-list'); + function exportBillingReport() { + const period = document.getElementById("billingPeriod")?.value || "current"; + showNotification("Generating billing report...", "info"); + + fetch(`/api/admin/billing/export?period=${period}`) + .then((response) => { + if (response.ok) return response.blob(); + throw new Error("Export failed"); + }) + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `billing-report-${period}.csv`; + a.click(); + URL.revokeObjectURL(url); + showNotification("Report downloaded", "success"); + }) + .catch((err) => + showNotification("Export failed: " + err.message, "error"), + ); + } + + function toggleBreakdownView() { + const chart = document.getElementById("breakdown-chart"); + const table = document.getElementById("breakdown-table"); + + if (chart && table) { + const showingChart = !chart.classList.contains("hidden"); + chart.classList.toggle("hidden", showingChart); + table.classList.toggle("hidden", !showingChart); + } + } + + function showQuotaSettings() { + showModal("quota-settings-modal"); + } + + function closeQuotaSettings() { + hideModal("quota-settings-modal"); + } + + function saveQuotaSettings() { + const form = document.getElementById("quota-form"); + if (!form) return; + + const formData = new FormData(form); + const quotas = Object.fromEntries(formData); + + fetch("/api/admin/billing/quotas", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(quotas), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Quota settings saved", "success"); + closeQuotaSettings(); + } else { + showNotification("Failed to save: " + data.error, "error"); } - } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } - function exportAuditLog() { - const category = document.getElementById('logCategory')?.value || 'all'; - showNotification('Exporting audit log...', 'info'); + function configureAlerts() { + showModal("alerts-config-modal"); + } - fetch(`/api/compliance/audit-log/export?category=${category}`) - .then(response => { - if (response.ok) return response.blob(); - throw new Error('Export failed'); - }) - .then(blob => { - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'audit-log.csv'; - a.click(); - URL.revokeObjectURL(url); - showNotification('Audit log exported', 'success'); - }) - .catch(err => showNotification('Export failed: ' + err.message, 'error')); - } + function closeAlertsConfig() { + hideModal("alerts-config-modal"); + } - // ============================================================================= - // GROUPS.HTML FUNCTIONS - // ============================================================================= + function saveAlertSettings() { + const form = document.getElementById("alerts-form"); + if (!form) return; - function closeDetailPanel() { - const panel = document.getElementById('detail-panel'); - if (panel) { - panel.classList.remove('open'); + const formData = new FormData(form); + const settings = Object.fromEntries(formData); + + fetch("/api/admin/billing/alerts", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(settings), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Alert settings saved", "success"); + closeAlertsConfig(); } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } + + // ============================================================================= + // BILLING.HTML FUNCTIONS + // ============================================================================= + + function showUpgradeModal() { + showModal("upgrade-modal"); + } + + function closeUpgradeModal() { + hideModal("upgrade-modal"); + } + + function showCancelModal() { + showModal("cancel-modal"); + } + + function closeCancelModal() { + hideModal("cancel-modal"); + } + + function showAddPaymentModal() { + showModal("add-payment-modal"); + } + + function closeAddPaymentModal() { + hideModal("add-payment-modal"); + } + + function showEditAddressModal() { + showModal("edit-address-modal"); + } + + function closeEditAddressModal() { + hideModal("edit-address-modal"); + } + + function exportInvoices() { + showNotification("Exporting invoices...", "info"); + + fetch("/api/billing/invoices/export") + .then((response) => { + if (response.ok) return response.blob(); + throw new Error("Export failed"); + }) + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "invoices.csv"; + a.click(); + URL.revokeObjectURL(url); + showNotification("Invoices exported", "success"); + }) + .catch((err) => + showNotification("Export failed: " + err.message, "error"), + ); + } + + function contactSales() { + window.open( + "mailto:sales@example.com?subject=Enterprise Plan Inquiry", + "_blank", + ); + } + + function showDowngradeOptions() { + closeCancelModal(); + showUpgradeModal(); + // Focus on lower-tier plans + const planSelector = document.querySelector(".plan-options"); + if (planSelector) { + planSelector.scrollIntoView({ behavior: "smooth" }); + } + } + + function selectPlan(planId) { + document.querySelectorAll(".plan-option").forEach((el) => { + el.classList.toggle("selected", el.dataset.plan === planId); + }); + } + + function confirmUpgrade() { + const selectedPlan = document.querySelector(".plan-option.selected"); + if (!selectedPlan) { + showNotification("Please select a plan", "error"); + return; } - function openDetailPanel(groupId) { - const panel = document.getElementById('detail-panel'); - if (panel) { - panel.classList.add('open'); - // Load group details - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', `/api/admin/groups/${groupId}`, '#panel-content'); - } + const planId = selectedPlan.dataset.plan; + + fetch("/api/billing/subscription/upgrade", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ plan_id: planId }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Plan upgraded successfully!", "success"); + closeUpgradeModal(); + location.reload(); + } else { + showNotification("Upgrade failed: " + data.error, "error"); } - } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } - function createGroup() { - showModal('create-group-modal'); - } + function confirmCancellation() { + const reason = document.getElementById("cancel-reason")?.value; - function closeCreateGroup() { - hideModal('create-group-modal'); - } - - function saveGroup() { - const name = document.getElementById('group-name')?.value; - const description = document.getElementById('group-description')?.value; - - if (!name) { - showNotification('Please enter a group name', 'error'); - return; + fetch("/api/billing/subscription/cancel", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ reason }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Subscription cancelled", "success"); + closeCancelModal(); + location.reload(); + } else { + showNotification("Cancellation failed: " + data.error, "error"); } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } - fetch('/api/admin/groups', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name, description }) - }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('Group created', 'success'); - closeCreateGroup(); - location.reload(); - } else { - showNotification('Failed to create group: ' + data.error, 'error'); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); + // ============================================================================= + // COMPLIANCE-DASHBOARD.HTML FUNCTIONS + // ============================================================================= + + function updateFramework(framework) { + // Update dashboard for selected compliance framework + if (typeof htmx !== "undefined") { + htmx.ajax( + "GET", + `/api/compliance/dashboard?framework=${framework}`, + "#compliance-content", + ); + } else { + fetch(`/api/compliance/dashboard?framework=${framework}`) + .then((r) => r.json()) + .then((data) => updateComplianceDashboard(data)) + .catch((err) => console.error("Failed to update framework:", err)); + } + } + + function updateComplianceDashboard(data) { + // Update various dashboard elements + if (data.score) { + const el = document.getElementById("compliance-score"); + if (el) el.textContent = data.score + "%"; + } + } + + function generateComplianceReport() { + const framework = + document.getElementById("complianceFramework")?.value || "soc2"; + showNotification("Generating compliance report...", "info"); + + fetch(`/api/compliance/report?framework=${framework}`) + .then((response) => { + if (response.ok) return response.blob(); + throw new Error("Report generation failed"); + }) + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `compliance-report-${framework}.pdf`; + a.click(); + URL.revokeObjectURL(url); + showNotification("Report generated", "success"); + }) + .catch((err) => + showNotification("Report failed: " + err.message, "error"), + ); + } + + function startAuditPrep() { + showModal("audit-prep-modal"); + } + + function closeAuditPrep() { + hideModal("audit-prep-modal"); + } + + function showEvidenceUpload() { + showModal("evidence-upload-modal"); + } + + function closeEvidenceUpload() { + hideModal("evidence-upload-modal"); + } + + function uploadEvidence() { + const fileInput = document.getElementById("evidence-file"); + const category = document.getElementById("evidence-category")?.value; + + if (!fileInput?.files?.length) { + showNotification("Please select a file", "error"); + return; } - function deleteGroup(groupId) { - if (!confirm('Delete this group? This action cannot be undone.')) return; + const formData = new FormData(); + formData.append("file", fileInput.files[0]); + formData.append("category", category); - fetch(`/api/admin/groups/${groupId}`, { method: 'DELETE' }) - .then(response => response.json()) - .then(data => { - if (data.success) { - showNotification('Group deleted', 'success'); - closeDetailPanel(); - location.reload(); - } - }) - .catch(err => showNotification('Error: ' + err.message, 'error')); + fetch("/api/compliance/evidence", { + method: "POST", + body: formData, + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Evidence uploaded", "success"); + closeEvidenceUpload(); + } else { + showNotification("Upload failed: " + data.error, "error"); + } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } + + function filterLogs() { + const category = document.getElementById("logCategory")?.value || "all"; + + if (typeof htmx !== "undefined") { + htmx.ajax( + "GET", + `/api/compliance/audit-log?category=${category}`, + "#audit-log-list", + ); + } + } + + function exportAuditLog() { + const category = document.getElementById("logCategory")?.value || "all"; + showNotification("Exporting audit log...", "info"); + + fetch(`/api/compliance/audit-log/export?category=${category}`) + .then((response) => { + if (response.ok) return response.blob(); + throw new Error("Export failed"); + }) + .then((blob) => { + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = "audit-log.csv"; + a.click(); + URL.revokeObjectURL(url); + showNotification("Audit log exported", "success"); + }) + .catch((err) => + showNotification("Export failed: " + err.message, "error"), + ); + } + + // ============================================================================= + // GROUPS.HTML FUNCTIONS + // ============================================================================= + + function closeDetailPanel() { + const panel = document.getElementById("detail-panel"); + if (panel) { + panel.classList.remove("open"); + } + } + + function openDetailPanel(groupId) { + const panel = document.getElementById("detail-panel"); + if (panel) { + panel.classList.add("open"); + // Load group details + if (typeof htmx !== "undefined") { + htmx.ajax("GET", `/api/admin/groups/${groupId}`, "#panel-content"); + } + } + } + + function createGroup() { + showModal("create-group-modal"); + } + + function closeCreateGroup() { + hideModal("create-group-modal"); + } + + function saveGroup() { + const name = document.getElementById("group-name")?.value; + const description = document.getElementById("group-description")?.value; + + if (!name) { + showNotification("Please enter a group name", "error"); + return; } - // ============================================================================= - // UTILITY FUNCTIONS - // ============================================================================= + fetch("/api/admin/groups", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, description }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Group created", "success"); + closeCreateGroup(); + location.reload(); + } else { + showNotification("Failed to create group: " + data.error, "error"); + } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } - function formatCurrency(amount, currency = 'USD') { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: currency - }).format(amount); + function deleteGroup(groupId) { + if (!confirm("Delete this group? This action cannot be undone.")) return; + + fetch(`/api/admin/groups/${groupId}`, { method: "DELETE" }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Group deleted", "success"); + closeDetailPanel(); + location.reload(); + } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } + + // ============================================================================= + // ROLE MANAGEMENT FUNCTIONS (roles.html) + // ============================================================================= + + let currentRole = null; + let availablePermissions = []; + let assignedPermissions = []; + + function selectRole(roleId, element) { + // Update UI selection + document + .querySelectorAll(".role-item") + .forEach((item) => item.classList.remove("selected")); + if (element) element.classList.add("selected"); + + // Show role detail + document + .getElementById("role-placeholder") + ?.style.setProperty("display", "none"); + document + .getElementById("role-detail") + ?.style.setProperty("display", "block"); + + // Fetch role details + fetch(`/api/rbac/roles/${roleId}`) + .then((response) => response.json()) + .then((data) => { + currentRole = data; + renderRoleDetail(data); + }) + .catch((err) => + showNotification("Failed to load role: " + err.message, "error"), + ); + } + + function renderRoleDetail(role) { + document.getElementById("role-display-name").textContent = + role.displayName || role.name; + document.getElementById("role-name").textContent = role.name; + + const typeBadge = document.getElementById("role-type-badge"); + if (typeBadge) { + typeBadge.textContent = role.isSystem ? "System" : "Custom"; + typeBadge.className = + "role-type-badge " + (role.isSystem ? "system" : "custom"); } - // ============================================================================= - // EXPORT TO WINDOW - // ============================================================================= + // Enable/disable delete button based on system role + const deleteBtn = document.getElementById("btn-delete-role"); + if (deleteBtn) deleteBtn.disabled = role.isSystem; - // Accounts - window.showSmtpModal = showSmtpModal; - window.closeSmtpModal = closeSmtpModal; - window.testSmtpConnection = testSmtpConnection; - window.connectAccount = connectAccount; - window.disconnectAccount = disconnectAccount; + // Load permissions + loadRolePermissions(role.id); + } - // Admin Dashboard - window.showInviteMemberModal = showInviteMemberModal; - window.closeInviteMemberModal = closeInviteMemberModal; - window.showBulkInviteModal = showBulkInviteModal; - window.closeBulkInviteModal = closeBulkInviteModal; - window.sendInvitation = sendInvitation; - window.sendBulkInvitations = sendBulkInvitations; - window.resendInvitation = resendInvitation; - window.cancelInvitation = cancelInvitation; + function loadRolePermissions(roleId) { + Promise.all([ + fetch("/api/rbac/permissions").then((r) => r.json()), + fetch(`/api/rbac/roles/${roleId}/permissions`).then((r) => r.json()), + ]) + .then(([allPerms, rolePerms]) => { + assignedPermissions = rolePerms || []; + availablePermissions = (allPerms || []).filter( + (p) => !assignedPermissions.find((rp) => rp.id === p.id), + ); + renderPermissionLists(); + }) + .catch((err) => console.error("Failed to load permissions:", err)); + } - // Billing Dashboard - window.updateBillingPeriod = updateBillingPeriod; - window.exportBillingReport = exportBillingReport; - window.toggleBreakdownView = toggleBreakdownView; - window.showQuotaSettings = showQuotaSettings; - window.closeQuotaSettings = closeQuotaSettings; - window.saveQuotaSettings = saveQuotaSettings; - window.configureAlerts = configureAlerts; - window.closeAlertsConfig = closeAlertsConfig; - window.saveAlertSettings = saveAlertSettings; + function renderPermissionLists() { + const availableList = document.getElementById("available-permissions"); + const assignedList = document.getElementById("assigned-permissions"); - // Billing - window.showUpgradeModal = showUpgradeModal; - window.closeUpgradeModal = closeUpgradeModal; - window.showCancelModal = showCancelModal; - window.closeCancelModal = closeCancelModal; - window.showAddPaymentModal = showAddPaymentModal; - window.closeAddPaymentModal = closeAddPaymentModal; - window.showEditAddressModal = showEditAddressModal; - window.closeEditAddressModal = closeEditAddressModal; - window.exportInvoices = exportInvoices; - window.contactSales = contactSales; - window.showDowngradeOptions = showDowngradeOptions; - window.selectPlan = selectPlan; - window.confirmUpgrade = confirmUpgrade; - window.confirmCancellation = confirmCancellation; + if (availableList) { + availableList.innerHTML = + availablePermissions + .map( + (p) => ` +
+ ${p.name} + ${p.scope || "global"} +
+ `, + ) + .join("") || + '
No available permissions
'; + } - // Compliance Dashboard - window.updateFramework = updateFramework; - window.generateComplianceReport = generateComplianceReport; - window.startAuditPrep = startAuditPrep; - window.closeAuditPrep = closeAuditPrep; - window.showEvidenceUpload = showEvidenceUpload; - window.closeEvidenceUpload = closeEvidenceUpload; - window.uploadEvidence = uploadEvidence; - window.filterLogs = filterLogs; - window.exportAuditLog = exportAuditLog; + if (assignedList) { + assignedList.innerHTML = + assignedPermissions + .map( + (p) => ` +
+ ${p.name} + ${p.scope || "global"} +
+ `, + ) + .join("") || '
No assigned permissions
'; + } + } - // Groups - window.closeDetailPanel = closeDetailPanel; - window.openDetailPanel = openDetailPanel; - window.createGroup = createGroup; - window.closeCreateGroup = closeCreateGroup; - window.saveGroup = saveGroup; - window.deleteGroup = deleteGroup; + function togglePermissionSelect(element) { + element.classList.toggle("selected"); + } + function assignSelected() { + const selected = document.querySelectorAll( + "#available-permissions .permission-item.selected", + ); + selected.forEach((item) => { + const id = item.dataset.id; + const perm = availablePermissions.find((p) => p.id === id); + if (perm) { + availablePermissions = availablePermissions.filter((p) => p.id !== id); + assignedPermissions.push(perm); + } + }); + renderPermissionLists(); + } + + function assignAll() { + assignedPermissions = [...assignedPermissions, ...availablePermissions]; + availablePermissions = []; + renderPermissionLists(); + } + + function removeSelected() { + const selected = document.querySelectorAll( + "#assigned-permissions .permission-item.selected", + ); + selected.forEach((item) => { + const id = item.dataset.id; + const perm = assignedPermissions.find((p) => p.id === id); + if (perm) { + assignedPermissions = assignedPermissions.filter((p) => p.id !== id); + availablePermissions.push(perm); + } + }); + renderPermissionLists(); + } + + function removeAll() { + availablePermissions = [...availablePermissions, ...assignedPermissions]; + assignedPermissions = []; + renderPermissionLists(); + } + + function savePermissions() { + if (!currentRole) { + showNotification("No role selected", "error"); + return; + } + + const permissionIds = assignedPermissions.map((p) => p.id); + + fetch(`/api/rbac/roles/${currentRole.id}/permissions`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ permissions: permissionIds }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Permissions saved successfully", "success"); + } else { + showNotification( + "Failed to save permissions: " + (data.error || "Unknown error"), + "error", + ); + } + }) + .catch((err) => + showNotification("Error saving permissions: " + err.message, "error"), + ); + } + + function resetPermissions() { + if (!currentRole) return; + if ( + !confirm( + "Reset permissions to default? This will undo any unsaved changes.", + ) + ) + return; + loadRolePermissions(currentRole.id); + showNotification("Permissions reset to saved state", "info"); + } + + function duplicateRole() { + if (!currentRole) { + showNotification("No role selected", "error"); + return; + } + + const newName = prompt( + "Enter name for the new role:", + currentRole.name + "_copy", + ); + if (!newName) return; + + fetch("/api/rbac/roles", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: newName, + displayName: currentRole.displayName + " (Copy)", + description: currentRole.description, + permissions: assignedPermissions.map((p) => p.id), + }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success || data.id) { + showNotification("Role duplicated successfully", "success"); + location.reload(); + } else { + showNotification( + "Failed to duplicate role: " + (data.error || "Unknown error"), + "error", + ); + } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } + + function confirmDeleteRole() { + if (!currentRole) return; + if (currentRole.isSystem) { + showNotification("System roles cannot be deleted", "error"); + return; + } + showModal("delete-role-modal"); + } + + function deleteRole() { + if (!currentRole) return; + + fetch(`/api/rbac/roles/${currentRole.id}`, { method: "DELETE" }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Role deleted", "success"); + hideModal("delete-role-modal"); + location.reload(); + } else { + showNotification( + "Failed to delete role: " + (data.error || "Unknown error"), + "error", + ); + } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } + + function assignUsersToRole() { + const selectedUsers = Array.from( + document.querySelectorAll("#user-assign-list input:checked"), + ).map((input) => input.value); + + if (selectedUsers.length === 0) { + showNotification("Please select at least one user", "error"); + return; + } + + if (!currentRole) { + showNotification("No role selected", "error"); + return; + } + + fetch(`/api/rbac/roles/${currentRole.id}/users`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ userIds: selectedUsers }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification( + `${selectedUsers.length} user(s) assigned to role`, + "success", + ); + hideModal("assign-users-modal"); + } else { + showNotification( + "Failed to assign users: " + (data.error || "Unknown error"), + "error", + ); + } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } + + function assignGroupsToRole() { + const selectedGroups = Array.from( + document.querySelectorAll("#group-assign-list input:checked"), + ).map((input) => input.value); + + if (selectedGroups.length === 0) { + showNotification("Please select at least one group", "error"); + return; + } + + if (!currentRole) { + showNotification("No role selected", "error"); + return; + } + + fetch(`/api/rbac/roles/${currentRole.id}/groups`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ groupIds: selectedGroups }), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification( + `${selectedGroups.length} group(s) assigned to role`, + "success", + ); + hideModal("assign-groups-modal"); + } else { + showNotification( + "Failed to assign groups: " + (data.error || "Unknown error"), + "error", + ); + } + }) + .catch((err) => showNotification("Error: " + err.message, "error")); + } + + function filterRoles(type) { + const items = document.querySelectorAll(".role-item"); + items.forEach((item) => { + const isSystem = item.dataset.system === "true"; + if (type === "all") { + item.style.display = ""; + } else if (type === "system") { + item.style.display = isSystem ? "" : "none"; + } else if (type === "custom") { + item.style.display = isSystem ? "none" : ""; + } + }); + } + + // ============================================================================= + // BILLING ADMIN FUNCTIONS + // ============================================================================= + + function downloadInvoice(invoiceId) { + showNotification(`Downloading invoice ${invoiceId}...`, "info"); + + fetch(`/api/billing/invoices/${invoiceId}/download`) + .then((response) => { + if (response.ok) { + return response.blob(); + } + throw new Error("Download failed"); + }) + .then((blob) => { + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = `${invoiceId}.pdf`; + link.click(); + URL.revokeObjectURL(url); + showNotification("Invoice downloaded", "success"); + }) + .catch((err) => + showNotification("Failed to download: " + err.message, "error"), + ); + } + + function dismissAlert(button) { + const alertItem = button.closest(".alert-item"); + if (alertItem) { + alertItem.style.opacity = "0"; + alertItem.style.transform = "translateX(100%)"; + setTimeout(() => alertItem.remove(), 300); + } + } + + function viewEvidence(evidenceId) { + showNotification(`Loading evidence: ${evidenceId}...`, "info"); + + fetch(`/api/compliance/evidence/${evidenceId}`) + .then((response) => response.json()) + .then((data) => { + // Show evidence in modal or new window + if (data.url) { + window.open(data.url, "_blank"); + } else { + showModal("evidence-view-modal"); + const content = document.getElementById("evidence-content"); + if (content) { + content.innerHTML = ` +

${data.name || evidenceId}

+

${data.description || "No description available"}

+
+ Type: ${data.type || "Document"} + Uploaded: ${data.uploadedAt || "Unknown"} +
+ `; + } + } + }) + .catch((err) => + showNotification("Failed to load evidence: " + err.message, "error"), + ); + } + + // ============================================================================= + // OPERATIONS DASHBOARD FUNCTIONS + // ============================================================================= + + let autoRefreshEnabled = true; + let autoRefreshInterval = null; + + function toggleAutoRefresh() { + autoRefreshEnabled = !autoRefreshEnabled; + const label = document.getElementById("autoRefreshLabel"); + if (label) { + label.textContent = `Auto-refresh: ${autoRefreshEnabled ? "ON" : "OFF"}`; + } + + if (autoRefreshEnabled) { + startAutoRefresh(); + } else { + stopAutoRefresh(); + } + } + + function startAutoRefresh() { + if (autoRefreshInterval) clearInterval(autoRefreshInterval); + autoRefreshInterval = setInterval(() => { + refreshHealth(); + }, 30000); + } + + function stopAutoRefresh() { + if (autoRefreshInterval) { + clearInterval(autoRefreshInterval); + autoRefreshInterval = null; + } + } + + function showAlertConfig() { + showModal("alert-config-modal"); + } + + function closeAlertConfig() { + hideModal("alert-config-modal"); + } + + function showTraceDetail(traceId) { + showNotification(`Loading trace ${traceId}...`, "info"); + + fetch(`/api/ops/traces/${traceId}`) + .then((response) => response.json()) + .then((data) => { + showModal("trace-detail-modal"); + const content = document.getElementById("trace-detail-content"); + if (content) { + content.innerHTML = ` +
+

${data.name || traceId}

+ ${data.status || "Success"} +
+
+
+ + ${data.traceId || traceId} +
+
+ + ${data.duration || "0"}ms +
+
+ + ${data.spanCount || 0} +
+
+ + ${data.service || "unknown"} +
+
+
+ ${(data.spans || []) + .map( + (span) => ` +
+ ${span.name} + ${span.duration}ms +
+ `, + ) + .join("")} +
+ `; + } + }) + .catch((err) => + showNotification("Failed to load trace: " + err.message, "error"), + ); + } + + function refreshHealth() { + const healthGrid = document.querySelector(".health-grid"); + if (healthGrid) { + healthGrid.innerHTML = + '
'; + } + + fetch("/api/ops/health") + .then((response) => response.json()) + .then((data) => { + if (healthGrid) { + healthGrid.innerHTML = + (data.services || []) + .map( + (service) => ` +
+
+ + ${service.name} +
+
+ Latency: ${service.latency || 0}ms + Uptime: ${service.uptime || "100%"} +
+
+ `, + ) + .join("") || '
No services found
'; + } + }) + .catch((err) => { + if (healthGrid) { + healthGrid.innerHTML = + '
Failed to load health data
'; + } + }); + } + + // ============================================================================= + // ONBOARDING FUNCTIONS + // ============================================================================= + + let currentStep = 1; + + function nextStep(step) { + const currentPanel = document.querySelector( + `.onboarding-panel[data-step="${step}"]`, + ); + const nextPanel = document.querySelector( + `.onboarding-panel[data-step="${step + 1}"]`, + ); + + if (currentPanel) currentPanel.classList.remove("active"); + if (nextPanel) nextPanel.classList.add("active"); + + currentStep = step + 1; + updateStepIndicators(); + } + + function prevStep(step) { + const currentPanel = document.querySelector( + `.onboarding-panel[data-step="${step}"]`, + ); + const prevPanel = document.querySelector( + `.onboarding-panel[data-step="${step - 1}"]`, + ); + + if (currentPanel) currentPanel.classList.remove("active"); + if (prevPanel) prevPanel.classList.add("active"); + + currentStep = step - 1; + updateStepIndicators(); + } + + function updateStepIndicators() { + document.querySelectorAll(".step-indicator").forEach((indicator, index) => { + const stepNum = index + 1; + indicator.classList.remove("active", "completed"); + if (stepNum < currentStep) { + indicator.classList.add("completed"); + } else if (stepNum === currentStep) { + indicator.classList.add("active"); + } + }); + } + + function skipPayment() { + showNotification("Skipping payment setup - Free plan selected", "info"); + nextStep(4); + } + + function removeLogo() { + const logoPreview = document.getElementById("logo-preview"); + const logoInput = document.getElementById("logo-input"); + + if (logoPreview) { + logoPreview.innerHTML = + 'No logo uploaded'; + } + if (logoInput) { + logoInput.value = ""; + } + showNotification("Logo removed", "success"); + } + + function previewLogo(input) { + if (input.files && input.files[0]) { + const reader = new FileReader(); + reader.onload = function (e) { + const preview = document.getElementById("logo-preview"); + if (preview) { + preview.innerHTML = `Logo preview`; + } + }; + reader.readAsDataURL(input.files[0]); + } + } + + // ============================================================================= + // ACCOUNTS LIST FUNCTIONS (sources/accounts-list.html) + // ============================================================================= + + function editAccount(accountId) { + showNotification(`Loading account ${accountId}...`, "info"); + + fetch(`/api/sources/accounts/${accountId}`) + .then((response) => response.json()) + .then((data) => { + showModal("edit-account-modal"); + // Populate form fields + const form = document.getElementById("edit-account-form"); + if (form) { + form + .querySelector('[name="account-name"]') + ?.setAttribute("value", data.name || ""); + form + .querySelector('[name="account-email"]') + ?.setAttribute("value", data.email || ""); + } + }) + .catch((err) => + showNotification("Failed to load account: " + err.message, "error"), + ); + } + + function syncAccount(accountId) { + showNotification(`Syncing account ${accountId}...`, "info"); + + fetch(`/api/sources/accounts/${accountId}/sync`, { method: "POST" }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Account sync started", "success"); + } else { + showNotification( + "Sync failed: " + (data.error || "Unknown error"), + "error", + ); + } + }) + .catch((err) => showNotification("Sync error: " + err.message, "error")); + } + + // ============================================================================= + // SEARCH SETTINGS FUNCTIONS (admin/search-settings.html) + // ============================================================================= + + function openReindexModal() { + const modal = document.getElementById("reindex-modal"); + if (modal) { + modal.showModal(); + } + } + + function closeReindexModal() { + const modal = document.getElementById("reindex-modal"); + if (modal) { + modal.close(); + } + } + + function startReindex() { + showNotification("Starting reindex operation...", "info"); + + fetch("/api/ui/sources/kb/reindex", { method: "POST" }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Reindex started successfully", "success"); + closeReindexModal(); + refreshStats(); + } else { + showNotification( + "Reindex failed: " + (data.error || "Unknown error"), + "error", + ); + } + }) + .catch((err) => + showNotification("Reindex error: " + err.message, "error"), + ); + } + + function refreshStats() { + showNotification("Refreshing search statistics...", "info"); + + fetch("/api/ui/sources/kb/stats") + .then((response) => response.json()) + .then((data) => { + // Update stats display + const totalDocs = document.getElementById("total-documents"); + const indexSize = document.getElementById("index-size"); + const lastIndexed = document.getElementById("last-indexed"); + + if (totalDocs) totalDocs.textContent = data.totalDocuments || "0"; + if (indexSize) indexSize.textContent = data.indexSize || "0 MB"; + if (lastIndexed) lastIndexed.textContent = data.lastIndexed || "Never"; + + showNotification("Statistics updated", "success"); + }) + .catch((err) => + showNotification("Failed to refresh stats: " + err.message, "error"), + ); + } + + function saveSearchSettings() { + const form = document.getElementById("search-settings-form"); + if (!form) { + showNotification("Settings form not found", "error"); + return; + } + + const formData = new FormData(form); + const settings = Object.fromEntries(formData.entries()); + + showNotification("Saving search settings...", "info"); + + fetch("/api/settings/search", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(settings), + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + showNotification("Search settings saved successfully", "success"); + } else { + showNotification( + "Failed to save settings: " + (data.error || "Unknown error"), + "error", + ); + } + }) + .catch((err) => showNotification("Save error: " + err.message, "error")); + } + + // ============================================================================= + // UTILITY FUNCTIONS + // ============================================================================= + + function formatCurrency(amount, currency = "USD") { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: currency, + }).format(amount); + } + + // ============================================================================= + // EXPORT TO WINDOW + // ============================================================================= + + // Accounts + window.showSmtpModal = showSmtpModal; + window.closeSmtpModal = closeSmtpModal; + window.testSmtpConnection = testSmtpConnection; + window.connectAccount = connectAccount; + window.disconnectAccount = disconnectAccount; + + // Admin Dashboard + window.showInviteMemberModal = showInviteMemberModal; + window.closeInviteMemberModal = closeInviteMemberModal; + window.showBulkInviteModal = showBulkInviteModal; + window.closeBulkInviteModal = closeBulkInviteModal; + window.sendInvitation = sendInvitation; + window.sendBulkInvitations = sendBulkInvitations; + window.resendInvitation = resendInvitation; + window.cancelInvitation = cancelInvitation; + + // Billing Dashboard + window.updateBillingPeriod = updateBillingPeriod; + window.exportBillingReport = exportBillingReport; + window.toggleBreakdownView = toggleBreakdownView; + window.showQuotaSettings = showQuotaSettings; + window.closeQuotaSettings = closeQuotaSettings; + window.saveQuotaSettings = saveQuotaSettings; + window.configureAlerts = configureAlerts; + window.closeAlertsConfig = closeAlertsConfig; + window.saveAlertSettings = saveAlertSettings; + + // Billing + window.showUpgradeModal = showUpgradeModal; + window.closeUpgradeModal = closeUpgradeModal; + window.showCancelModal = showCancelModal; + window.closeCancelModal = closeCancelModal; + window.showAddPaymentModal = showAddPaymentModal; + window.closeAddPaymentModal = closeAddPaymentModal; + window.showEditAddressModal = showEditAddressModal; + window.closeEditAddressModal = closeEditAddressModal; + window.exportInvoices = exportInvoices; + window.contactSales = contactSales; + window.showDowngradeOptions = showDowngradeOptions; + window.selectPlan = selectPlan; + window.confirmUpgrade = confirmUpgrade; + window.confirmCancellation = confirmCancellation; + + // Compliance Dashboard + window.updateFramework = updateFramework; + window.generateComplianceReport = generateComplianceReport; + window.startAuditPrep = startAuditPrep; + window.closeAuditPrep = closeAuditPrep; + window.showEvidenceUpload = showEvidenceUpload; + window.closeEvidenceUpload = closeEvidenceUpload; + window.uploadEvidence = uploadEvidence; + window.filterLogs = filterLogs; + window.exportAuditLog = exportAuditLog; + + // Groups + window.closeDetailPanel = closeDetailPanel; + window.openDetailPanel = openDetailPanel; + window.createGroup = createGroup; + window.closeCreateGroup = closeCreateGroup; + window.saveGroup = saveGroup; + window.deleteGroup = deleteGroup; + + // Role Management + window.selectRole = selectRole; + window.assignSelected = assignSelected; + window.assignAll = assignAll; + window.removeSelected = removeSelected; + window.removeAll = removeAll; + window.savePermissions = savePermissions; + window.resetPermissions = resetPermissions; + window.duplicateRole = duplicateRole; + window.confirmDeleteRole = confirmDeleteRole; + window.deleteRole = deleteRole; + window.assignUsersToRole = assignUsersToRole; + window.assignGroupsToRole = assignGroupsToRole; + window.filterRoles = filterRoles; + + // Billing Admin + window.downloadInvoice = downloadInvoice; + window.dismissAlert = dismissAlert; + window.viewEvidence = viewEvidence; + + // Operations Dashboard + window.toggleAutoRefresh = toggleAutoRefresh; + window.showAlertConfig = showAlertConfig; + window.closeAlertConfig = closeAlertConfig; + window.showTraceDetail = showTraceDetail; + window.refreshHealth = refreshHealth; + + // Onboarding + window.nextStep = nextStep; + window.prevStep = prevStep; + window.skipPayment = skipPayment; + window.removeLogo = removeLogo; + window.previewLogo = previewLogo; + + // Account Management + window.editAccount = editAccount; + window.syncAccount = syncAccount; + + // Search Settings + window.openReindexModal = openReindexModal; + window.closeReindexModal = closeReindexModal; + window.startReindex = startReindex; + window.refreshStats = refreshStats; + window.saveSearchSettings = saveSearchSettings; })(); diff --git a/ui/suite/canvas/canvas.js b/ui/suite/canvas/canvas.js index 23b5a3a..b029387 100644 --- a/ui/suite/canvas/canvas.js +++ b/ui/suite/canvas/canvas.js @@ -1065,6 +1065,251 @@ } } + // ============================================================================= + // SHARING & COLLABORATION + // ============================================================================= + + function shareCanvas() { + if (!state.canvasId) { + // Save canvas first if not saved + saveCanvas().then(() => { + showShareDialog(); + }); + } else { + showShareDialog(); + } + } + + function showShareDialog() { + const modal = document.getElementById("share-modal"); + if (modal) { + if (modal.showModal) { + modal.showModal(); + } else { + modal.classList.add("open"); + modal.style.display = "flex"; + } + // Generate share link + const shareUrl = `${window.location.origin}/canvas?id=${state.canvasId}`; + const shareLinkInput = document.getElementById("share-link"); + if (shareLinkInput) { + shareLinkInput.value = shareUrl; + } + } else { + // Fallback: copy link to clipboard + const shareUrl = `${window.location.origin}/canvas?id=${state.canvasId || "new"}`; + navigator.clipboard + .writeText(shareUrl) + .then(() => { + showNotification("Share link copied to clipboard", "success"); + }) + .catch(() => { + showNotification( + "Canvas ID: " + (state.canvasId || "unsaved"), + "info", + ); + }); + } + } + + // ============================================================================= + // PROPERTIES PANEL + // ============================================================================= + + function togglePropertiesPanel() { + const panel = document.getElementById("properties-panel"); + if (panel) { + panel.classList.toggle("collapsed"); + const isCollapsed = panel.classList.contains("collapsed"); + // Update toggle button icon if needed + const toggleBtn = panel.querySelector(".panel-toggle span"); + if (toggleBtn) { + toggleBtn.textContent = isCollapsed ? "⚙️" : "✕"; + } + } + } + + // ============================================================================= + // LAYERS MANAGEMENT + // ============================================================================= + + let layers = [ + { id: "layer_1", name: "Layer 1", visible: true, locked: false }, + ]; + let activeLayerId = "layer_1"; + + function addLayer() { + const newId = "layer_" + (layers.length + 1); + const newLayer = { + id: newId, + name: "Layer " + (layers.length + 1), + visible: true, + locked: false, + }; + layers.push(newLayer); + activeLayerId = newId; + renderLayers(); + showNotification("Layer added", "success"); + } + + function renderLayers() { + const layersList = document.getElementById("layers-list"); + if (!layersList) return; + + layersList.innerHTML = layers + .map( + (layer) => ` +
+ ${layer.visible ? "👁️" : "👁️‍🗨️"} + ${layer.name} + ${layer.locked ? "🔒" : "🔓"} +
+ `, + ) + .join(""); + } + + function selectLayer(layerId) { + activeLayerId = layerId; + renderLayers(); + } + + function toggleLayerVisibility(layerId) { + const layer = layers.find((l) => l.id === layerId); + if (layer) { + layer.visible = !layer.visible; + renderLayers(); + render(); + } + } + + function toggleLayerLock(layerId) { + const layer = layers.find((l) => l.id === layerId); + if (layer) { + layer.locked = !layer.locked; + renderLayers(); + } + } + + // ============================================================================= + // CLIPBOARD & DUPLICATE + // ============================================================================= + + function duplicateSelected() { + if (!state.selectedElement) { + showNotification("No element selected", "warning"); + return; + } + + const original = state.selectedElement; + const duplicate = JSON.parse(JSON.stringify(original)); + duplicate.id = generateId(); + // Offset the duplicate slightly + if (duplicate.x !== undefined) duplicate.x += 20; + if (duplicate.y !== undefined) duplicate.y += 20; + + state.elements.push(duplicate); + state.selectedElement = duplicate; + saveToHistory(); + render(); + showNotification("Element duplicated", "success"); + } + + function copySelected() { + if (!state.selectedElement) { + showNotification("No element selected", "warning"); + return; + } + state.clipboard = JSON.parse(JSON.stringify(state.selectedElement)); + showNotification("Element copied", "success"); + } + + function pasteClipboard() { + if (!state.clipboard) { + showNotification("Nothing to paste", "warning"); + return; + } + + const pasted = JSON.parse(JSON.stringify(state.clipboard)); + pasted.id = generateId(); + // Offset the pasted element + if (pasted.x !== undefined) pasted.x += 20; + if (pasted.y !== undefined) pasted.y += 20; + + state.elements.push(pasted); + state.selectedElement = pasted; + saveToHistory(); + render(); + showNotification("Element pasted", "success"); + } + + // ============================================================================= + // ELEMENT ORDERING + // ============================================================================= + + function bringToFront() { + if (!state.selectedElement) return; + const index = state.elements.findIndex( + (e) => e.id === state.selectedElement.id, + ); + if (index !== -1 && index < state.elements.length - 1) { + state.elements.splice(index, 1); + state.elements.push(state.selectedElement); + saveToHistory(); + render(); + } + } + + function sendToBack() { + if (!state.selectedElement) return; + const index = state.elements.findIndex( + (e) => e.id === state.selectedElement.id, + ); + if (index > 0) { + state.elements.splice(index, 1); + state.elements.unshift(state.selectedElement); + saveToHistory(); + render(); + } + } + + // ============================================================================= + // EXPORT MODAL + // ============================================================================= + + function showExportModal() { + const modal = document.getElementById("export-modal"); + if (modal) { + if (modal.showModal) { + modal.showModal(); + } else { + modal.classList.add("open"); + modal.style.display = "flex"; + } + } + } + + function closeExportModal() { + const modal = document.getElementById("export-modal"); + if (modal) { + if (modal.close) { + modal.close(); + } else { + modal.classList.remove("open"); + modal.style.display = "none"; + } + } + } + + function doExport() { + const formatSelect = document.getElementById("export-format"); + const format = formatSelect ? formatSelect.value : "png"; + exportCanvas(format); + closeExportModal(); + } + // ============================================================================= // UTILITIES // ============================================================================= @@ -1108,6 +1353,32 @@ window.cutElement = cutElement; window.pasteElement = pasteElement; + // Sharing & Collaboration + window.shareCanvas = shareCanvas; + + // Properties Panel + window.togglePropertiesPanel = togglePropertiesPanel; + + // Layers + window.addLayer = addLayer; + window.selectLayer = selectLayer; + window.toggleLayerVisibility = toggleLayerVisibility; + window.toggleLayerLock = toggleLayerLock; + + // Clipboard & Duplicate + window.duplicateSelected = duplicateSelected; + window.copySelected = copySelected; + window.pasteClipboard = pasteClipboard; + + // Element Ordering + window.bringToFront = bringToFront; + window.sendToBack = sendToBack; + + // Export Modal + window.showExportModal = showExportModal; + window.closeExportModal = closeExportModal; + window.doExport = doExport; + // ============================================================================= // INITIALIZE // ============================================================================= diff --git a/ui/suite/chat/chat.css b/ui/suite/chat/chat.css index 2f949fc..8b086b4 100644 --- a/ui/suite/chat/chat.css +++ b/ui/suite/chat/chat.css @@ -110,12 +110,97 @@ /* Messages Area */ #messages { - flex: 1; - overflow-y: auto; - padding: 20px 0; - display: flex; - flex-direction: column; - gap: 16px; +flex: 1; +overflow-y: auto; +padding: 20px 0; +display: flex; +flex-direction: column; +gap: 16px; +scrollbar-width: thin; +scrollbar-color: var(--accent, #3b82f6) var(--surface, #1a1a24); +} + +/* Custom scrollbar for markers */ +#messages::-webkit-scrollbar { +width: 6px; +} + +#messages::-webkit-scrollbar-track { +background: var(--surface, #1a1a24); +border-radius: 3px; +} + +#messages::-webkit-scrollbar-thumb { +background: var(--accent, #3b82f6); +border-radius: 3px; +border: 1px solid var(--surface, #1a1a24); +} + +#messages::-webkit-scrollbar-thumb:hover { +background: var(--accent-hover, #2563eb); +} + +/* Scrollbar markers container */ +.scrollbar-markers { +position: absolute; +top: 0; +right: 2px; +width: 8px; +height: 100%; +pointer-events: none; +z-index: 10; +} + +.scrollbar-marker { +position: absolute; +right: 0; +width: 8px; +height: 8px; +background: var(--accent, #3b82f6); +border-radius: 50%; +cursor: pointer; +pointer-events: auto; +transition: all 0.2s ease; +box-shadow: 0 0 4px rgba(0, 0, 0, 0.5); +z-index: 11; +} + +.scrollbar-marker:hover { +transform: scale(1.5); +background: var(--accent-hover, #2563eb); +box-shadow: 0 0 8px var(--accent, #3b82f6); +} + +.scrollbar-marker.user-marker { +background: var(--accent, #3b82f6); +} + +.scrollbar-marker.bot-marker { +background: var(--success, #22c55e); +} + +.scrollbar-marker-tooltip { +position: absolute; +right: 12px; +transform: translateY(-50%); +background: var(--surface, #1a1a24); +border: 1px solid var(--border, #2a2a2a); +border-radius: 6px; +padding: 4px 8px; +font-size: 11px; +color: var(--text, #ffffff); +white-space: nowrap; +opacity: 0; +visibility: hidden; +transition: all 0.2s ease; +pointer-events: none; +z-index: 12; +box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); +} + +.scrollbar-marker:hover .scrollbar-marker-tooltip { +opacity: 1; +visibility: visible; } /* Message Styles */ @@ -314,8 +399,28 @@ footer { } .mention-results { - overflow-y: auto; - max-height: 250px; +overflow-y: auto; +max-height: 250px; +scrollbar-width: thin; +scrollbar-color: var(--accent, #3b82f6) transparent; +} + +.mention-results::-webkit-scrollbar { +width: 4px; +} + +.mention-results::-webkit-scrollbar-track { +background: transparent; +border-radius: 2px; +} + +.mention-results::-webkit-scrollbar-thumb { +background: var(--accent, #3b82f6); +border-radius: 2px; +} + +.mention-results::-webkit-scrollbar-thumb:hover { +background: var(--accent-hover, #2563eb); } .mention-item { diff --git a/ui/suite/drive/drive-sentient.js b/ui/suite/drive/drive-sentient.js new file mode 100644 index 0000000..26009d9 --- /dev/null +++ b/ui/suite/drive/drive-sentient.js @@ -0,0 +1,296 @@ +(function () { + "use strict"; + + let currentView = "grid"; + let selectedFile = null; + let aiPanelOpen = true; + + function toggleView(type) { + currentView = type; + const fileView = document.getElementById("file-view"); + if (fileView) { + fileView.classList.remove("grid-view", "list-view"); + fileView.classList.add(type + "-view"); + } + document.querySelectorAll(".app-btn-secondary").forEach((btn) => { + btn.classList.remove("active"); + }); + event.target.classList.add("active"); + } + + function openFolder(el) { + const folderName = el.querySelector(".file-name").textContent; + const breadcrumb = document.querySelector(".breadcrumb"); + if (breadcrumb) { + const separator = document.createElement("span"); + separator.className = "breadcrumb-separator"; + separator.textContent = "›"; + breadcrumb.appendChild(separator); + + const item = document.createElement("span"); + item.className = "breadcrumb-item current"; + item.textContent = folderName; + breadcrumb.appendChild(item); + + breadcrumb.querySelectorAll(".breadcrumb-item").forEach((i) => { + i.classList.remove("current"); + }); + item.classList.add("current"); + } + addAIMessage("assistant", `Opened folder: ${folderName}`); + } + + function selectFile(el) { + document.querySelectorAll(".file-item").forEach((item) => { + item.classList.remove("selected"); + }); + el.classList.add("selected"); + selectedFile = { + name: el.querySelector(".file-name").textContent, + meta: el.querySelector(".file-meta").textContent, + }; + } + + function toggleAIPanel() { + const panel = document.getElementById("ai-panel"); + if (panel) { + aiPanelOpen = !aiPanelOpen; + panel.classList.toggle("hidden", !aiPanelOpen); + } + const toggle = document.querySelector(".ai-toggle"); + if (toggle) { + toggle.classList.toggle("active", aiPanelOpen); + } + } + + function aiAction(action) { + const actions = { + organize: "Analyzing folder structure to suggest organization...", + find: "What file are you looking for?", + analyze: "Select a file and I'll analyze its content.", + share: "Select a file to set up sharing options.", + }; + addAIMessage("assistant", actions[action] || "How can I help?"); + } + + function sendAIMessage() { + const input = document.getElementById("ai-input"); + if (!input || !input.value.trim()) return; + + const message = input.value.trim(); + addAIMessage("user", message); + input.value = ""; + + setTimeout(() => { + processAIQuery(message); + }, 500); + } + + function addAIMessage(type, content) { + const container = document.getElementById("ai-messages"); + if (!container) return; + + const div = document.createElement("div"); + div.className = "ai-message " + type; + div.innerHTML = '
' + escapeHtml(content) + "
"; + container.appendChild(div); + container.scrollTop = container.scrollHeight; + } + + function processAIQuery(query) { + const lowerQuery = query.toLowerCase(); + let response = "I can help you manage your files. Try asking me to find, organize, or analyze files."; + + if (lowerQuery.includes("find") || lowerQuery.includes("search") || lowerQuery.includes("buscar")) { + response = "I'll search for files matching your query. What type of file are you looking for?"; + } else if (lowerQuery.includes("organize") || lowerQuery.includes("organizar")) { + response = "I can help organize your files by type, date, or project. Which method would you prefer?"; + } else if (lowerQuery.includes("share") || lowerQuery.includes("compartilhar")) { + if (selectedFile) { + response = `Setting up sharing for "${selectedFile.name}". Who would you like to share it with?`; + } else { + response = "Please select a file first, then I can help you share it."; + } + } else if (lowerQuery.includes("delete") || lowerQuery.includes("excluir")) { + if (selectedFile) { + response = `Are you sure you want to delete "${selectedFile.name}"? This action cannot be undone.`; + } else { + response = "Please select a file first before deleting."; + } + } else if (lowerQuery.includes("storage") || lowerQuery.includes("space") || lowerQuery.includes("espaço")) { + response = "You're using 12.4 GB of your 50 GB storage. Would you like me to find large files to free up space?"; + } + + addAIMessage("assistant", response); + } + + function uploadFile() { + const input = document.createElement("input"); + input.type = "file"; + input.multiple = true; + input.onchange = function (e) { + const files = Array.from(e.target.files); + if (files.length > 0) { + const names = files.map((f) => f.name).join(", "); + addAIMessage("assistant", `Uploading ${files.length} file(s): ${names}`); + simulateUpload(files); + } + }; + input.click(); + } + + function simulateUpload(files) { + setTimeout(() => { + addAIMessage("assistant", `Successfully uploaded ${files.length} file(s)!`); + files.forEach((file) => { + addFileToView(file.name, formatFileSize(file.size)); + }); + }, 1500); + } + + function addFileToView(name, size) { + const fileView = document.getElementById("file-view"); + if (!fileView) return; + + const icon = getFileIcon(name); + const div = document.createElement("div"); + div.className = "file-item"; + div.onclick = function () { + selectFile(this); + }; + div.innerHTML = ` +
${icon}
+
${escapeHtml(name)}
+
${size}
+ `; + fileView.appendChild(div); + } + + function getFileIcon(filename) { + const ext = filename.split(".").pop().toLowerCase(); + const icons = { + pdf: "📄", + doc: "📝", + docx: "📝", + xls: "📊", + xlsx: "📊", + ppt: "📽️", + pptx: "📽️", + jpg: "🖼️", + jpeg: "🖼️", + png: "🖼️", + gif: "🖼️", + mp4: "🎬", + mov: "🎬", + mp3: "🎵", + wav: "🎵", + zip: "📦", + rar: "📦", + txt: "📝", + md: "📝", + js: "💻", + ts: "💻", + rs: "💻", + py: "💻", + }; + return icons[ext] || "📄"; + } + + function formatFileSize(bytes) { + if (bytes < 1024) return bytes + " B"; + if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB"; + if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + " MB"; + return (bytes / (1024 * 1024 * 1024)).toFixed(1) + " GB"; + } + + function escapeHtml(text) { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + function initKeyboardShortcuts() { + document.addEventListener("keydown", function (e) { + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") return; + + if (e.key === "Delete" && selectedFile) { + addAIMessage("assistant", `Delete "${selectedFile.name}"? Press Delete again to confirm.`); + } + if (e.ctrlKey && e.key === "u") { + e.preventDefault(); + uploadFile(); + } + if (e.key === "Escape") { + document.querySelectorAll(".file-item").forEach((item) => { + item.classList.remove("selected"); + }); + selectedFile = null; + } + }); + } + + function initSearch() { + const searchInput = document.querySelector(".search-input"); + if (searchInput) { + searchInput.addEventListener("input", function (e) { + const query = e.target.value.toLowerCase(); + document.querySelectorAll(".file-item").forEach((item) => { + const name = item.querySelector(".file-name").textContent.toLowerCase(); + item.style.display = name.includes(query) ? "" : "none"; + }); + }); + } + } + + function initTabs() { + document.querySelectorAll(".topbar-tab").forEach((tab) => { + tab.addEventListener("click", function () { + document.querySelectorAll(".topbar-tab").forEach((t) => t.classList.remove("active")); + this.classList.add("active"); + const tabName = this.textContent.trim(); + addAIMessage("assistant", `Switched to ${tabName} view.`); + }); + }); + } + + function initAppLauncher() { + document.querySelectorAll(".app-icon").forEach((icon) => { + icon.addEventListener("click", function () { + const app = this.dataset.app; + if (app && app !== "drive") { + window.location.href = "/suite/" + app + "/"; + } + }); + }); + } + + function init() { + initKeyboardShortcuts(); + initSearch(); + initTabs(); + initAppLauncher(); + + const aiInput = document.getElementById("ai-input"); + if (aiInput) { + aiInput.addEventListener("keypress", function (e) { + if (e.key === "Enter") { + sendAIMessage(); + } + }); + } + } + + window.toggleView = toggleView; + window.openFolder = openFolder; + window.selectFile = selectFile; + window.toggleAIPanel = toggleAIPanel; + window.aiAction = aiAction; + window.sendAIMessage = sendAIMessage; + window.uploadFile = uploadFile; + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/ui/suite/learn/learn.js b/ui/suite/learn/learn.js index a7c9c5e..c729b5a 100644 --- a/ui/suite/learn/learn.js +++ b/ui/suite/learn/learn.js @@ -5,121 +5,123 @@ // 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 - } + 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'; +const LEARN_API = "/api/learn"; // ============================================================================ // INITIALIZATION // ============================================================================ -document.addEventListener('DOMContentLoaded', () => { - initLearn(); +document.addEventListener("DOMContentLoaded", () => { + initLearn(); }); function initLearn() { - loadUserStats(); - loadCategories(); - loadCourses(); - loadMyCourses(); - loadMandatoryAssignments(); - loadCertificates(); - loadRecommendations(); - bindEvents(); + 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); - }); + // 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(); } - - // 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(); - } - }); + }); } // ============================================================================ @@ -127,192 +129,193 @@ function bindEvents() { // ============================================================================ async function apiCall(endpoint, options = {}) { - try { - const response = await fetch(`${LEARN_API}${endpoint}`, { - headers: { - 'Content-Type': 'application/json', - ...options.headers - }, - ...options - }); + try { + const response = await fetch(`${LEARN_API}${endpoint}`, { + headers: { + "Content-Type": "application/json", + ...options.headers, + }, + ...options, + }); - const data = await response.json(); + 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; + 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(); + 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 + 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 - }); + 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(); + 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(); + 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(); + 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(); + 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(); + 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); + 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.'); + 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.'); + 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.'); + 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."); + } } // ============================================================================ @@ -320,74 +323,80 @@ async function submitQuizAPI(courseId, answers) { // ============================================================================ 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`; + 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; - }); + // 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; + 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'); + const grid = document.getElementById("coursesGrid"); + const countLabel = document.getElementById("coursesCountLabel"); - if (!grid) return; + if (!grid) return; - if (LearnState.pagination.offset === 0) { - grid.innerHTML = ''; - } + if (LearnState.pagination.offset === 0) { + grid.innerHTML = ""; + } - if (LearnState.courses.length === 0) { - grid.innerHTML = ` + if (LearnState.courses.length === 0) { + grid.innerHTML = `
📚

Nenhum curso encontrado

Tente ajustar os filtros de busca.

`; - countLabel.textContent = '0 cursos'; - return; - } + countLabel.textContent = "0 cursos"; + return; + } - LearnState.courses.forEach(course => { - grid.appendChild(createCourseCard(course)); - }); + LearnState.courses.forEach((course) => { + grid.appendChild(createCourseCard(course)); + }); - countLabel.textContent = `${LearnState.courses.length} cursos`; + 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'; - } + // 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 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; + const difficultyClass = (course.difficulty || "beginner").toLowerCase(); + const progress = course.user_progress || 0; - card.innerHTML = ` + card.innerHTML = `
- ${course.thumbnail_url + ${ + course.thumbnail_url ? `${course.title}` : `📖` } - ${course.is_mandatory ? 'Obrigatório' : ''} - ${progress > 0 ? `${progress}%` : ''} + ${course.is_mandatory ? 'Obrigatório' : ""} + ${progress > 0 ? `${progress}%` : ""}

${escapeHtml(course.title)}

@@ -401,65 +410,77 @@ function createCourseCard(course) { ${formatDuration(course.duration_minutes)}
- ${progress > 0 ? ` + ${ + progress > 0 + ? `
${progress}% completo
- ` : ''} + ` + : "" + } `; - return card; + return card; } function renderMyCourses() { - const continueLearning = document.getElementById('continueLearning'); - const completedCourses = document.getElementById('completedCourses'); + const continueLearning = document.getElementById("continueLearning"); + const completedCourses = document.getElementById("completedCourses"); - if (!continueLearning || !completedCourses) return; + if (!continueLearning || !completedCourses) return; - const inProgress = LearnState.myCourses.filter(c => c.status === 'in_progress'); - const completed = LearnState.myCourses.filter(c => c.status === 'completed'); + 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; + // Update badge + document.getElementById("myCoursesCount").textContent = inProgress.length; - // Render in-progress courses - if (inProgress.length === 0) { - continueLearning.innerHTML = ` + // Render in-progress courses + if (inProgress.length === 0) { + continueLearning.innerHTML = `
📚

Nenhum curso em andamento

`; - } else { - continueLearning.innerHTML = inProgress.map(course => createCourseListItem(course)).join(''); - } + } else { + continueLearning.innerHTML = inProgress + .map((course) => createCourseListItem(course)) + .join(""); + } - // Render completed courses - if (completed.length === 0) { - completedCourses.innerHTML = ` + // Render completed courses + if (completed.length === 0) { + completedCourses.innerHTML = `

Nenhum curso concluído ainda

`; - } else { - completedCourses.innerHTML = completed.map(course => createCourseListItem(course, true)).join(''); - } + } else { + completedCourses.innerHTML = completed + .map((course) => createCourseListItem(course, true)) + .join(""); + } } function createCourseListItem(course, isCompleted = false) { - return ` + return `
📖
-

${escapeHtml(course.course_title || course.title || 'Curso')}

+

${escapeHtml(course.course_title || course.title || "Curso")}

${formatDuration(course.duration_minutes || 30)}
@@ -472,7 +493,7 @@ function createCourseListItem(course, isCompleted = false) {
@@ -480,93 +501,102 @@ function createCourseListItem(course, isCompleted = false) { } function renderMandatoryAssignments() { - const list = document.getElementById('mandatoryList'); - const badge = document.getElementById('mandatoryCount'); + const list = document.getElementById("mandatoryList"); + const badge = document.getElementById("mandatoryCount"); - if (!list) return; + if (!list) return; - badge.textContent = LearnState.mandatoryAssignments.length; + badge.textContent = LearnState.mandatoryAssignments.length; - if (LearnState.mandatoryAssignments.length === 0) { - list.innerHTML = ` + if (LearnState.mandatoryAssignments.length === 0) { + list.innerHTML = `
🎉

Tudo em dia!

Você não possui treinamentos obrigatórios pendentes.

`; - return; - } + 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; + 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 ` -
- ${isOverdue ? '⚠️' : (isUrgent ? '⏰' : '📋')} + ${isOverdue ? "⚠️" : isUrgent ? "⏰" : "📋"}
-

${escapeHtml(assignment.course_title || 'Treinamento Obrigatório')}

-
- ${isOverdue - ? '⚠️ Prazo vencido!' - : (daysUntilDue !== null - ? `Prazo: ${daysUntilDue} dias` - : 'Sem prazo definido' - ) +

${escapeHtml(assignment.course_title || "Treinamento Obrigatório")}

+
+ ${ + isOverdue + ? "⚠️ Prazo vencido!" + : daysUntilDue !== null + ? `Prazo: ${daysUntilDue} dias` + : "Sem prazo definido" }
`; - }).join(''); + }) + .join(""); } function updateMandatoryAlert() { - const alert = document.getElementById('mandatoryAlert'); - const alertText = document.getElementById('mandatoryAlertText'); + const alert = document.getElementById("mandatoryAlert"); + const alertText = document.getElementById("mandatoryAlertText"); - if (!alert) return; + if (!alert) return; - const overdueCount = LearnState.mandatoryAssignments.filter(a => - a.due_date && new Date(a.due_date) < new Date() - ).length; + 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; + 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.`; - } + if (overdueCount > 0 || urgentCount > 0) { + alert.style.display = "flex"; + if (overdueCount > 0) { + alertText.textContent = `Você possui ${overdueCount} treinamento(s) com prazo vencido!`; } else { - alert.style.display = 'none'; + 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'); + const grid = document.getElementById("certificatesGrid"); + const preview = document.getElementById("certificatesPreview"); - if (!grid) return; + if (!grid) return; - if (LearnState.certificates.length === 0) { - grid.innerHTML = ` + if (LearnState.certificates.length === 0) { + grid.innerHTML = `
🏆

Nenhum certificado ainda

@@ -574,22 +604,24 @@ function renderCertificates() {
`; - if (preview) { - preview.innerHTML = ` + if (preview) { + preview.innerHTML = `
🏆

Nenhum certificado ainda

`; - } - return; } + return; + } - grid.innerHTML = LearnState.certificates.map(cert => ` + grid.innerHTML = LearnState.certificates + .map( + (cert) => `
🎓 -

${escapeHtml(cert.course_title || 'Curso Concluído')}

+

${escapeHtml(cert.course_title || "Curso Concluído")}

${cert.score}% @@ -609,36 +641,47 @@ function renderCertificates() {
- `).join(''); + `, + ) + .join(""); - // Update sidebar preview - if (preview) { - preview.innerHTML = LearnState.certificates.slice(0, 3).map(cert => ` + // Update sidebar preview + if (preview) { + preview.innerHTML = LearnState.certificates + .slice(0, 3) + .map( + (cert) => `
🎓
-
${escapeHtml(cert.course_title || 'Curso')}
+
${escapeHtml(cert.course_title || "Curso")}
${formatDate(cert.issued_at)}
- `).join(''); - } + `, + ) + .join(""); + } } function renderRecommendations(courses) { - const carousel = document.getElementById('recommendedCourses'); - if (!carousel) return; + const carousel = document.getElementById("recommendedCourses"); + if (!carousel) return; - if (!courses || courses.length === 0) { - carousel.innerHTML = '

Explore o catálogo para encontrar cursos.

'; - return; - } + if (!courses || courses.length === 0) { + carousel.innerHTML = + '

Explore o catálogo para encontrar cursos.

'; + return; + } - carousel.innerHTML = courses.slice(0, 6).map(course => { - const card = createCourseCard(course); - card.style.minWidth = '280px'; - return card.outerHTML; - }).join(''); + carousel.innerHTML = courses + .slice(0, 6) + .map((course) => { + const card = createCourseCard(course); + card.style.minWidth = "280px"; + return card.outerHTML; + }) + .join(""); } // ============================================================================ @@ -646,32 +689,41 @@ function renderRecommendations(courses) { // ============================================================================ function openCourseModal(courseId) { - loadCourseDetail(courseId); - document.getElementById('courseModal').classList.remove('hidden'); + loadCourseDetail(courseId); + document.getElementById("courseModal").classList.remove("hidden"); } function closeCourseModal() { - document.getElementById('courseModal').classList.add('hidden'); - LearnState.currentCourse = null; + document.getElementById("courseModal").classList.add("hidden"); + LearnState.currentCourse = null; } function renderCourseModal(data) { - const { course, lessons, quiz } = 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`; + 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) => ` -
0) { + lessonsList.innerHTML = lessons + .map( + (lesson, index) => ` +
- ${lesson.is_completed ? '✓' : index + 1} + ${lesson.is_completed ? "✓" : index + 1}
${escapeHtml(lesson.title)}
${formatDuration(lesson.duration_minutes)} @@ -682,143 +734,424 @@ function renderCourseModal(data) {
- `).join(''); - } else { - lessonsList.innerHTML = '

Nenhuma aula disponível.

'; - } + `, + ) + .join(""); + } else { + lessonsList.innerHTML = + '

Nenhuma aula disponível.

'; + } - // 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'; - } + // 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 = ` + // 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 = ` Revisar Curso `; - } else if (progress.status === 'in_progress') { - startBtn.innerHTML = ` + } else if (progress.status === "in_progress") { + startBtn.innerHTML = ` Continuar `; - } + } - // 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 = ` + // 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 = ` Iniciar Curso `; - } + } } function startCourse() { - if (!LearnState.currentCourse) return; + if (!LearnState.currentCourse) return; - const course = LearnState.currentCourse.course || LearnState.currentCourse; - const lessons = LearnState.currentCourse.lessons || []; + const course = LearnState.currentCourse.course || LearnState.currentCourse; + const lessons = LearnState.currentCourse.lessons || []; - // Start course via API - startCourseAPI(course.id); + // 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.'); - } + // 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]; + const lessons = LearnState.currentCourse?.lessons || []; + const lesson = lessons.find((l) => l.id === lessonId) || lessons[index]; - if (!lesson) { - showNotification('error', 'Aula não encontrada.'); - return; - } + if (!lesson) { + showNotification("error", "Aula não encontrada."); + return; + } - LearnState.currentLesson = lesson; - LearnState.currentLessonIndex = index; + LearnState.currentLesson = lesson; + LearnState.currentLessonIndex = index; - // Close course modal, open lesson modal - closeCourseModal(); - document.getElementById('lessonModal').classList.remove('hidden'); + // 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}`; + // 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 = ` + // Render content based on type + const contentDiv = document.getElementById("lessonContent"); + if (lesson.video_url) { + contentDiv.innerHTML = `
- ${lesson.content || ''} + ${lesson.content || ""}
`; - } else { - contentDiv.innerHTML = ` + } else { + contentDiv.innerHTML = `
- ${lesson.content || '

Conteúdo da aula será exibido aqui.

'} + ${lesson.content || "

Conteúdo da aula será exibido aqui.

"}
`; - } + } - // Update sidebar list - const sidebar = document.getElementById('lessonListSidebar'); - sidebar.innerHTML = lessons.map((l, i) => ` -
` +
- ${l.is_completed ? '✓' : i + 1} + ${l.is_completed ? "✓" : i + 1}
${escapeHtml(l.title)}
- `).join(''); + `, + ) + .join(""); - // Update navigation buttons - document.getElementById('prevLessonBtn').disabled = index === 0; - document.getElementById('nextLessonBtn').disabled = index >= lessons.length - 1; + // 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}%`; + // 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; + document.getElementById("lessonModal").classList.add("hidden"); + LearnState.currentLesson = null; - // Reopen course modal + // Reopen course modal + if (LearnState.currentCourse) { + openCourseModal(LearnState.currentCourse.id); + } +} + +function toggleLearnSidebar() { + const sidebar = document.querySelector(".learn-sidebar"); + if (sidebar) { + sidebar.classList.toggle("collapsed"); + } +} + +function switchTab(tabId) { + document.querySelectorAll(".tab-btn, .learn-tab-btn").forEach((btn) => { + btn.classList.remove("active"); + if ( + btn.dataset.tab === tabId || + btn.getAttribute("onclick")?.includes(tabId) + ) { + btn.classList.add("active"); + } + }); + + document + .querySelectorAll(".tab-content, .learn-tab-content") + .forEach((content) => { + content.classList.add("hidden"); + if (content.id === tabId || content.id === `${tabId}-tab`) { + content.classList.remove("hidden"); + } + }); +} + +function showAllCertificates() { + switchTab("certificates"); +} + +function loadMoreCourses() { + const currentCount = document.querySelectorAll(".course-card").length; + fetch(`/api/learn/courses?offset=${currentCount}&limit=12`) + .then((r) => r.json()) + .then((data) => { + const grid = document.querySelector(".courses-grid"); + if (grid && data.courses) { + data.courses.forEach((course) => { + grid.insertAdjacentHTML("beforeend", createCourseCard(course)); + }); + } + if (!data.hasMore) { + const btn = document.querySelector('[onclick="loadMoreCourses()"]'); + if (btn) btn.style.display = "none"; + } + }) + .catch((err) => console.error("Error loading more courses:", err)); +} + +function startQuiz() { + if (!LearnState.currentCourse) return; + + LearnState.quizState = { + questions: LearnState.currentCourse.quiz || [], + currentIndex: 0, + answers: {}, + startTime: Date.now(), + }; + + document.getElementById("courseModal").classList.add("hidden"); + document.getElementById("quizModal").classList.remove("hidden"); + renderQuizQuestion(); +} + +function renderQuizQuestion() { + const { questions, currentIndex } = LearnState.quizState; + if (!questions || questions.length === 0) return; + + const question = questions[currentIndex]; + const container = document.getElementById("quizQuestionContainer"); + + container.innerHTML = ` +
+
Question ${currentIndex + 1} of ${questions.length}
+

${question.text}

+
+ ${question.options + .map( + (opt, i) => ` + + `, + ) + .join("")} +
+
+ `; + + document.getElementById("quizProgress").textContent = + `${currentIndex + 1}/${questions.length}`; + document.getElementById("quizProgressFill").style.width = + `${((currentIndex + 1) / questions.length) * 100}%`; +} + +function selectAnswer(index) { + LearnState.quizState.answers[LearnState.quizState.currentIndex] = index; +} + +function prevQuestion() { + if (LearnState.quizState.currentIndex > 0) { + LearnState.quizState.currentIndex--; + renderQuizQuestion(); + } +} + +function nextQuestion() { + if ( + LearnState.quizState.currentIndex < + LearnState.quizState.questions.length - 1 + ) { + LearnState.quizState.currentIndex++; + renderQuizQuestion(); + } +} + +function submitQuiz() { + const { questions, answers, startTime } = LearnState.quizState; + let correct = 0; + + questions.forEach((q, i) => { + if (answers[i] === q.correctIndex) correct++; + }); + + const score = Math.round((correct / questions.length) * 100); + const passed = score >= 70; + const duration = Math.round((Date.now() - startTime) / 1000); + + LearnState.quizResult = { + score, + correct, + total: questions.length, + passed, + duration, + }; + + document.getElementById("quizModal").classList.add("hidden"); + document.getElementById("quizResultModal").classList.remove("hidden"); + + document.getElementById("quizScore").textContent = `${score}%`; + document.getElementById("quizCorrect").textContent = + `${correct}/${questions.length}`; + document.getElementById("quizStatus").textContent = passed + ? "Passed!" + : "Not Passed"; + document.getElementById("quizStatus").className = passed + ? "status-passed" + : "status-failed"; + + if (passed && LearnState.currentCourse) { + fetch(`/api/learn/courses/${LearnState.currentCourse.id}/complete`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ score, duration }), + }).catch((err) => console.error("Error completing course:", err)); + } +} + +function closeQuizResult() { + document.getElementById("quizResultModal").classList.add("hidden"); + LearnState.quizState = null; + LearnState.quizResult = null; + renderMyCourses(); + renderCertificates(); +} + +function reviewAnswers() { + document.getElementById("quizResultModal").classList.add("hidden"); + document.getElementById("quizModal").classList.remove("hidden"); + LearnState.quizState.currentIndex = 0; + renderQuizQuestion(); +} + +function confirmExitQuiz() { + if (confirm("Are you sure you want to exit? Your progress will be lost.")) { + document.getElementById("quizModal").classList.add("hidden"); + LearnState.quizState = null; + if (LearnState.currentCourse) { + openCourseModal(LearnState.currentCourse.id); + } + } +} + +function downloadCertificate() { + if (!LearnState.currentCourse) return; + window.open( + `/api/learn/certificates/${LearnState.currentCourse.id}/download`, + "_blank", + ); +} + +function downloadCertificateById(certId) { + window.open(`/api/learn/certificates/${certId}/download`, "_blank"); +} + +function closeCertificateModal() { + document.getElementById("certificateModal").classList.add("hidden"); +} + +function prevLesson() { + if (!LearnState.currentCourse || !LearnState.currentLesson) return; + const lessons = LearnState.currentCourse.lessons || []; + const currentIndex = lessons.findIndex( + (l) => l.id === LearnState.currentLesson.id, + ); + if (currentIndex > 0) { + openLesson(lessons[currentIndex - 1].id, currentIndex - 1); + } +} + +function nextLesson() { + if (!LearnState.currentCourse || !LearnState.currentLesson) return; + const lessons = LearnState.currentCourse.lessons || []; + const currentIndex = lessons.findIndex( + (l) => l.id === LearnState.currentLesson.id, + ); + if (currentIndex < lessons.length - 1) { + openLesson(lessons[currentIndex + 1].id, currentIndex + 1); + } +} + +function completeLesson() { + if (!LearnState.currentLesson || !LearnState.currentCourse) return; + + fetch(`/api/learn/lessons/${LearnState.currentLesson.id}/complete`, { + method: "POST", + }) + .then(() => { + LearnState.currentLesson.completed = true; + closeLessonModal(); + }) + .catch((err) => console.error("Error completing lesson:", err)); +} + +// Export functions to window +window.toggleLearnSidebar = toggleLearnSidebar; +window.showAllCertificates = showAllCertificates; +window.loadMoreCourses = loadMoreCourses; +window.startQuiz = startQuiz; +window.prevQuestion = prevQuestion; +window.nextQuestion = nextQuestion; +window.submitQuiz = submitQuiz; +window.closeQuizResult = closeQuizResult; +window.reviewAnswers = reviewAnswers; +window.confirmExitQuiz = confirmExitQuiz; +window.downloadCertificate = downloadCertificate; +window.downloadCertificateById = downloadCertificateById; +window.closeCertificateModal = closeCertificateModal; +window.prevLesson = prevLesson; +window.nextLesson = nextLesson; +window.completeLesson = completeLesson; +window.selectAnswer = selectAnswer; +window.switchTab = switchTab; +window.openCourseModal = openCourseModal; +window.closeCourseModal = closeCourseModal; +window.startCourse = startCourse; +window.openLesson = openLesson; +window.closeLessonModal = closeLessonModal; diff --git a/ui/suite/tools/tools.js b/ui/suite/tools/tools.js index d4ea839..634fde6 100644 --- a/ui/suite/tools/tools.js +++ b/ui/suite/tools/tools.js @@ -2,274 +2,650 @@ * Tools Module JavaScript * Compliance, Analytics, and Developer Tools */ -(function() { - 'use strict'; +(function () { + "use strict"; - /** - * Initialize the Tools module - */ - function init() { - setupBotSelector(); - setupFilters(); - setupKeyboardShortcuts(); - setupHTMXEvents(); + /** + * Initialize the Tools module + */ + function init() { + setupBotSelector(); + setupFilters(); + setupKeyboardShortcuts(); + setupHTMXEvents(); + updateStats(); + } + + /** + * Setup bot chip selection + */ + function setupBotSelector() { + document.addEventListener("click", function (e) { + const chip = e.target.closest(".bot-chip"); + if (chip) { + // Toggle selection + chip.classList.toggle("selected"); + + // Update hidden checkbox + const checkbox = chip.querySelector('input[type="checkbox"]'); + if (checkbox) { + checkbox.checked = chip.classList.contains("selected"); + } + + // Handle "All Bots" logic + if (chip.querySelector('input[value="all"]')) { + if (chip.classList.contains("selected")) { + // Deselect all other chips + document + .querySelectorAll(".bot-chip:not([data-all])") + .forEach((c) => { + c.classList.remove("selected"); + const cb = c.querySelector('input[type="checkbox"]'); + if (cb) cb.checked = false; + }); + } + } else { + // Deselect "All Bots" when selecting individual bots + const allChip = document + .querySelector('.bot-chip input[value="all"]') + ?.closest(".bot-chip"); + if (allChip) { + allChip.classList.remove("selected"); + const cb = allChip.querySelector('input[type="checkbox"]'); + if (cb) cb.checked = false; + } + } + } + }); + } + + /** + * Setup filter controls + */ + function setupFilters() { + // Filter select changes + document.querySelectorAll(".filter-select").forEach((select) => { + select.addEventListener("change", function () { + applyFilters(); + }); + }); + + // Search input + const searchInput = document.querySelector( + '.filter-input[name="filter-search"]', + ); + if (searchInput) { + let debounceTimer; + searchInput.addEventListener("input", function () { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => applyFilters(), 300); + }); + } + } + + /** + * Apply filters to results + */ + function applyFilters() { + const severity = document.getElementById("filter-severity")?.value || "all"; + const type = document.getElementById("filter-type")?.value || "all"; + const search = + document + .querySelector('.filter-input[name="filter-search"]') + ?.value.toLowerCase() || ""; + + const rows = document.querySelectorAll("#results-body tr"); + let visibleCount = 0; + + rows.forEach((row) => { + let visible = true; + + // Filter by severity + if (severity !== "all") { + const badge = row.querySelector(".severity-badge"); + if (badge && !badge.classList.contains(severity)) { + visible = false; + } + } + + // Filter by type + if (type !== "all" && visible) { + const issueIcon = row.querySelector(".issue-icon"); + if (issueIcon && !issueIcon.classList.contains(type)) { + visible = false; + } + } + + // Filter by search + if (search && visible) { + const text = row.textContent.toLowerCase(); + if (!text.includes(search)) { + visible = false; + } + } + + row.style.display = visible ? "" : "none"; + if (visible) visibleCount++; + }); + + // Update results count + const countEl = document.getElementById("results-count"); + if (countEl) { + countEl.textContent = `${visibleCount} issues found`; + } + } + + /** + * Setup keyboard shortcuts + */ + function setupKeyboardShortcuts() { + document.addEventListener("keydown", function (e) { + // Ctrl+Enter to run scan + if ((e.ctrlKey || e.metaKey) && e.key === "Enter") { + e.preventDefault(); + document.getElementById("scan-btn")?.click(); + } + + // Escape to close any open modals + if (e.key === "Escape") { + closeModals(); + } + + // Ctrl+E to export report + if ((e.ctrlKey || e.metaKey) && e.key === "e") { + e.preventDefault(); + exportReport(); + } + }); + } + + /** + * Setup HTMX events + */ + function setupHTMXEvents() { + if (typeof htmx === "undefined") return; + + document.body.addEventListener("htmx:afterSwap", function (e) { + if (e.detail.target.id === "scan-results") { updateStats(); - } + } + }); + } - /** - * Setup bot chip selection - */ - function setupBotSelector() { - document.addEventListener('click', function(e) { - const chip = e.target.closest('.bot-chip'); - if (chip) { - // Toggle selection - chip.classList.toggle('selected'); + /** + * Update statistics from results + */ + function updateStats() { + const rows = document.querySelectorAll("#results-body tr"); + let stats = { critical: 0, high: 0, medium: 0, low: 0, info: 0 }; - // Update hidden checkbox - const checkbox = chip.querySelector('input[type="checkbox"]'); - if (checkbox) { - checkbox.checked = chip.classList.contains('selected'); - } + rows.forEach((row) => { + if (row.style.display === "none") return; - // Handle "All Bots" logic - if (chip.querySelector('input[value="all"]')) { - if (chip.classList.contains('selected')) { - // Deselect all other chips - document.querySelectorAll('.bot-chip:not([data-all])').forEach(c => { - c.classList.remove('selected'); - const cb = c.querySelector('input[type="checkbox"]'); - if (cb) cb.checked = false; - }); - } - } else { - // Deselect "All Bots" when selecting individual bots - const allChip = document.querySelector('.bot-chip input[value="all"]')?.closest('.bot-chip'); - if (allChip) { - allChip.classList.remove('selected'); - const cb = allChip.querySelector('input[type="checkbox"]'); - if (cb) cb.checked = false; - } - } - } - }); - } + const badge = row.querySelector(".severity-badge"); + if (badge) { + if (badge.classList.contains("critical")) stats.critical++; + else if (badge.classList.contains("high")) stats.high++; + else if (badge.classList.contains("medium")) stats.medium++; + else if (badge.classList.contains("low")) stats.low++; + else if (badge.classList.contains("info")) stats.info++; + } + }); - /** - * Setup filter controls - */ - function setupFilters() { - // Filter select changes - document.querySelectorAll('.filter-select').forEach(select => { - select.addEventListener('change', function() { - applyFilters(); - }); - }); - - // Search input - const searchInput = document.querySelector('.filter-input[name="filter-search"]'); - if (searchInput) { - let debounceTimer; - searchInput.addEventListener('input', function() { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => applyFilters(), 300); - }); - } - } - - /** - * Apply filters to results - */ - function applyFilters() { - const severity = document.getElementById('filter-severity')?.value || 'all'; - const type = document.getElementById('filter-type')?.value || 'all'; - const search = document.querySelector('.filter-input[name="filter-search"]')?.value.toLowerCase() || ''; - - const rows = document.querySelectorAll('#results-body tr'); - let visibleCount = 0; - - rows.forEach(row => { - let visible = true; - - // Filter by severity - if (severity !== 'all') { - const badge = row.querySelector('.severity-badge'); - if (badge && !badge.classList.contains(severity)) { - visible = false; - } - } - - // Filter by type - if (type !== 'all' && visible) { - const issueIcon = row.querySelector('.issue-icon'); - if (issueIcon && !issueIcon.classList.contains(type)) { - visible = false; - } - } - - // Filter by search - if (search && visible) { - const text = row.textContent.toLowerCase(); - if (!text.includes(search)) { - visible = false; - } - } - - row.style.display = visible ? '' : 'none'; - if (visible) visibleCount++; - }); - - // Update results count - const countEl = document.getElementById('results-count'); - if (countEl) { - countEl.textContent = `${visibleCount} issues found`; - } - } - - /** - * Setup keyboard shortcuts - */ - function setupKeyboardShortcuts() { - document.addEventListener('keydown', function(e) { - // Ctrl+Enter to run scan - if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { - e.preventDefault(); - document.getElementById('scan-btn')?.click(); - } - - // Escape to close any open modals - if (e.key === 'Escape') { - closeModals(); - } - - // Ctrl+E to export report - if ((e.ctrlKey || e.metaKey) && e.key === 'e') { - e.preventDefault(); - exportReport(); - } - }); - } - - /** - * Setup HTMX events - */ - function setupHTMXEvents() { - if (typeof htmx === 'undefined') return; - - document.body.addEventListener('htmx:afterSwap', function(e) { - if (e.detail.target.id === 'scan-results') { - updateStats(); - } - }); - } - - /** - * Update statistics from results - */ - function updateStats() { - const rows = document.querySelectorAll('#results-body tr'); - let stats = { critical: 0, high: 0, medium: 0, low: 0, info: 0 }; - - rows.forEach(row => { - if (row.style.display === 'none') return; - - const badge = row.querySelector('.severity-badge'); - if (badge) { - if (badge.classList.contains('critical')) stats.critical++; - else if (badge.classList.contains('high')) stats.high++; - else if (badge.classList.contains('medium')) stats.medium++; - else if (badge.classList.contains('low')) stats.low++; - else if (badge.classList.contains('info')) stats.info++; - } - }); - - // Update stat cards - const updateStat = (id, value) => { - const el = document.getElementById(id); - if (el) el.textContent = value; - }; - - updateStat('stat-critical', stats.critical); - updateStat('stat-high', stats.high); - updateStat('stat-medium', stats.medium); - updateStat('stat-low', stats.low); - updateStat('stat-info', stats.info); - - // Update total count - const total = stats.critical + stats.high + stats.medium + stats.low + stats.info; - const countEl = document.getElementById('results-count'); - if (countEl) { - countEl.textContent = `${total} issues found`; - } - } - - /** - * Export compliance report - */ - function exportReport() { - if (typeof htmx !== 'undefined') { - htmx.ajax('GET', '/api/compliance/export', { - swap: 'none' - }); - } - } - - /** - * Fix an issue - */ - function fixIssue(issueId) { - if (typeof htmx !== 'undefined') { - htmx.ajax('POST', `/api/compliance/fix/${issueId}`, { - swap: 'none' - }).then(() => { - // Refresh results - const scanBtn = document.getElementById('scan-btn'); - if (scanBtn) scanBtn.click(); - }); - } - } - - /** - * Close all modals - */ - function closeModals() { - document.querySelectorAll('.modal').forEach(modal => { - modal.classList.add('hidden'); - }); - } - - /** - * Show toast notification - */ - function showToast(message, type = 'success') { - const toast = document.createElement('div'); - toast.className = `toast toast-${type}`; - toast.textContent = message; - document.body.appendChild(toast); - - requestAnimationFrame(() => { - toast.classList.add('show'); - }); - - setTimeout(() => { - toast.classList.remove('show'); - setTimeout(() => toast.remove(), 300); - }, 3000); - } - - // Initialize on DOM ready - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', init); - } else { - init(); - } - - // Expose for external use - window.Tools = { - updateStats, - applyFilters, - fixIssue, - exportReport, - showToast + // Update stat cards + const updateStat = (id, value) => { + const el = document.getElementById(id); + if (el) el.textContent = value; }; + + updateStat("stat-critical", stats.critical); + updateStat("stat-high", stats.high); + updateStat("stat-medium", stats.medium); + updateStat("stat-low", stats.low); + updateStat("stat-info", stats.info); + + // Update total count + const total = + stats.critical + stats.high + stats.medium + stats.low + stats.info; + const countEl = document.getElementById("results-count"); + if (countEl) { + countEl.textContent = `${total} issues found`; + } + } + + /** + * Export compliance report + */ + function exportReport() { + if (typeof htmx !== "undefined") { + htmx.ajax("GET", "/api/compliance/export", { + swap: "none", + }); + } + } + + /** + * Fix an issue + */ + function fixIssue(issueId) { + if (typeof htmx !== "undefined") { + htmx + .ajax("POST", `/api/compliance/fix/${issueId}`, { + swap: "none", + }) + .then(() => { + // Refresh results + const scanBtn = document.getElementById("scan-btn"); + if (scanBtn) scanBtn.click(); + }); + } + } + + /** + * Close all modals + */ + function closeModals() { + document.querySelectorAll(".modal").forEach((modal) => { + modal.classList.add("hidden"); + }); + } + + /** + * Show toast notification + */ + function showToast(message, type = "success") { + const toast = document.createElement("div"); + toast.className = `toast toast-${type}`; + toast.textContent = message; + document.body.appendChild(toast); + + requestAnimationFrame(() => { + toast.classList.add("show"); + }); + + setTimeout(() => { + toast.classList.remove("show"); + setTimeout(() => toast.remove(), 300); + }, 3000); + } + + // Initialize on DOM ready + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } + + /** + * Configure a protection tool + */ + function configureProtectionTool(toolName) { + const modal = + document.getElementById("configure-modal") || + document.getElementById("tool-config-modal"); + if (modal) { + const titleEl = modal.querySelector(".modal-title, h2, h3"); + if (titleEl) { + titleEl.textContent = `Configure ${toolName}`; + } + modal.dataset.tool = toolName; + if (modal.showModal) { + modal.showModal(); + } else { + modal.classList.remove("hidden"); + modal.style.display = "flex"; + } + } else { + showToast(`Opening configuration for ${toolName}...`, "info"); + fetch(`/api/tools/security/${toolName}/config`) + .then((r) => r.json()) + .then((config) => { + console.log(`${toolName} config:`, config); + showToast(`${toolName} configuration loaded`, "success"); + }) + .catch((err) => { + console.error(`Error loading ${toolName} config:`, err); + showToast(`Failed to load ${toolName} configuration`, "error"); + }); + } + } + + /** + * Run a protection tool scan + */ + function runProtectionTool(toolName) { + showToast(`Running ${toolName} scan...`, "info"); + + const statusEl = document.querySelector( + `[data-tool="${toolName}"] .tool-status, #${toolName}-status`, + ); + if (statusEl) { + statusEl.textContent = "Running..."; + statusEl.classList.add("running"); + } + + fetch(`/api/tools/security/${toolName}/run`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + }) + .then((r) => r.json()) + .then((result) => { + if (statusEl) { + statusEl.textContent = "Completed"; + statusEl.classList.remove("running"); + statusEl.classList.add("completed"); + } + showToast(`${toolName} scan completed`, "success"); + + if (result.report_url) { + viewReport(toolName); + } + }) + .catch((err) => { + console.error(`Error running ${toolName}:`, err); + if (statusEl) { + statusEl.textContent = "Error"; + statusEl.classList.remove("running"); + statusEl.classList.add("error"); + } + showToast(`Failed to run ${toolName}`, "error"); + }); + } + + /** + * Update a protection tool + */ + function updateProtectionTool(toolName) { + showToast(`Updating ${toolName}...`, "info"); + + fetch(`/api/tools/security/${toolName}/update`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + }) + .then((r) => r.json()) + .then((result) => { + showToast( + `${toolName} updated to version ${result.version || "latest"}`, + "success", + ); + + const versionEl = document.querySelector( + `[data-tool="${toolName}"] .tool-version, #${toolName}-version`, + ); + if (versionEl && result.version) { + versionEl.textContent = result.version; + } + }) + .catch((err) => { + console.error(`Error updating ${toolName}:`, err); + showToast(`Failed to update ${toolName}`, "error"); + }); + } + + /** + * View report for a protection tool + */ + function viewReport(toolName) { + const reportModal = + document.getElementById("report-modal") || + document.getElementById("view-report-modal"); + + if (reportModal) { + const titleEl = reportModal.querySelector(".modal-title, h2, h3"); + if (titleEl) { + titleEl.textContent = `${toolName} Report`; + } + + const contentEl = reportModal.querySelector( + ".report-content, .modal-body", + ); + if (contentEl) { + contentEl.innerHTML = '
Loading report...
'; + } + + if (reportModal.showModal) { + reportModal.showModal(); + } else { + reportModal.classList.remove("hidden"); + reportModal.style.display = "flex"; + } + + fetch(`/api/tools/security/${toolName}/report`) + .then((r) => r.json()) + .then((report) => { + if (contentEl) { + contentEl.innerHTML = renderReport(toolName, report); + } + }) + .catch((err) => { + console.error(`Error loading ${toolName} report:`, err); + if (contentEl) { + contentEl.innerHTML = + '
Failed to load report
'; + } + }); + } else { + window.open( + `/api/tools/security/${toolName}/report?format=html`, + "_blank", + ); + } + } + + /** + * Render a security tool report + */ + function renderReport(toolName, report) { + const findings = report.findings || []; + const summary = report.summary || {}; + + return ` +
+

Summary

+
+ ${summary.critical || 0} Critical + ${summary.high || 0} High + ${summary.medium || 0} Medium + ${summary.low || 0} Low +
+

Scan completed: ${report.completed_at || new Date().toISOString()}

+
+
+

Findings (${findings.length})

+ ${findings.length === 0 ? '

No issues found

' : ""} + ${findings + .map( + (f) => ` +
+ ${f.severity || "info"} + ${f.title || f.message || "Finding"} +

${f.description || ""}

+ ${f.remediation ? `

Fix: ${f.remediation}

` : ""} +
+ `, + ) + .join("")} +
+ `; + } + + /** + * Toggle auto action for a protection tool + */ + function toggleAutoAction(toolName, btn) { + const isEnabled = + btn.classList.contains("active") || + btn.getAttribute("aria-pressed") === "true"; + const newState = !isEnabled; + + fetch(`/api/tools/security/${toolName}/auto`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ enabled: newState }), + }) + .then((r) => r.json()) + .then((result) => { + if (newState) { + btn.classList.add("active"); + btn.setAttribute("aria-pressed", "true"); + showToast(`Auto-scan enabled for ${toolName}`, "success"); + } else { + btn.classList.remove("active"); + btn.setAttribute("aria-pressed", "false"); + showToast(`Auto-scan disabled for ${toolName}`, "info"); + } + }) + .catch((err) => { + console.error(`Error toggling auto action for ${toolName}:`, err); + showToast(`Failed to update ${toolName} settings`, "error"); + }); + } + + /** + * Reindex a data source for search + */ + function reindexSource(sourceName) { + showToast(`Reindexing ${sourceName}...`, "info"); + + fetch(`/api/search/reindex/${sourceName}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + }) + .then((r) => r.json()) + .then((result) => { + showToast( + `${sourceName} reindexing started. ${result.documents || 0} documents queued.`, + "success", + ); + }) + .catch((err) => { + console.error(`Error reindexing ${sourceName}:`, err); + showToast(`Failed to reindex ${sourceName}`, "error"); + }); + } + + /** + * Show TSC (Trust Service Criteria) details + */ + function showTscDetails(category) { + const detailPanel = + document.getElementById("tsc-detail-panel") || + document.querySelector(".tsc-details"); + + if (detailPanel) { + fetch(`/api/compliance/tsc/${category}`) + .then((r) => r.json()) + .then((data) => { + detailPanel.innerHTML = renderTscDetails(category, data); + detailPanel.classList.add("open"); + }) + .catch((err) => { + console.error(`Error loading TSC details for ${category}:`, err); + showToast(`Failed to load ${category} details`, "error"); + }); + } else { + showToast(`Viewing ${category} criteria...`, "info"); + } + } + + /** + * Render TSC details + */ + function renderTscDetails(category, data) { + const controls = data.controls || []; + return ` +
+

${category.charAt(0).toUpperCase() + category.slice(1)} Criteria

+ +
+
+ ${controls + .map( + (c) => ` +
+ ${c.id} + ${c.name} + ${c.status || "Pending"} +
+ `, + ) + .join("")} +
+ `; + } + + /** + * Show control remediation steps + */ + function showControlRemediation(controlId) { + const modal = + document.getElementById("remediation-modal") || + document.getElementById("control-modal"); + + if (modal) { + const titleEl = modal.querySelector(".modal-title, h2, h3"); + if (titleEl) { + titleEl.textContent = `Remediate ${controlId}`; + } + + const contentEl = modal.querySelector( + ".modal-body, .remediation-content", + ); + if (contentEl) { + contentEl.innerHTML = + '
Loading remediation steps...
'; + } + + if (modal.showModal) { + modal.showModal(); + } else { + modal.classList.remove("hidden"); + modal.style.display = "flex"; + } + + fetch(`/api/compliance/controls/${controlId}/remediation`) + .then((r) => r.json()) + .then((data) => { + if (contentEl) { + contentEl.innerHTML = ` +
+

Steps to Remediate

+
    + ${(data.steps || []).map((s) => `
  1. ${s}
  2. `).join("")} +
+ ${data.documentation_url ? `View Documentation` : ""} +
+ `; + } + }) + .catch((err) => { + console.error(`Error loading remediation for ${controlId}:`, err); + if (contentEl) { + contentEl.innerHTML = + '
Failed to load remediation steps
'; + } + }); + } else { + showToast(`Loading remediation for ${controlId}...`, "info"); + } + } + + // Expose for external use + window.Tools = { + updateStats, + applyFilters, + fixIssue, + exportReport, + showToast, + }; + + // Expose security tool functions globally + window.configureProtectionTool = configureProtectionTool; + window.runProtectionTool = runProtectionTool; + window.updateProtectionTool = updateProtectionTool; + window.viewReport = viewReport; + window.toggleAutoAction = toggleAutoAction; + window.reindexSource = reindexSource; + window.showTscDetails = showTscDetails; + window.showControlRemediation = showControlRemediation; })();