diff --git a/ui/suite/admin/admin-functions.js b/ui/suite/admin/admin-functions.js new file mode 100644 index 0000000..4dfaef4 --- /dev/null +++ b/ui/suite/admin/admin-functions.js @@ -0,0 +1,726 @@ +/* ============================================================================= + ADMIN MODULE - Missing Function Handlers + These functions are called by onclick handlers in admin HTML files + ============================================================================= */ + +(function() { + 'use strict'; + + // ============================================================================= + // 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 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; + } + + 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'); + } 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 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'); + } 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; + } + + const formData = new FormData(); + formData.append('file', fileInput.files[0]); + formData.append('category', category); + + 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; + } + + 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 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')); + } + + // ============================================================================= + // 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; + +})(); diff --git a/ui/suite/admin/index.html b/ui/suite/admin/index.html index 817676b..024ea60 100644 --- a/ui/suite/admin/index.html +++ b/ui/suite/admin/index.html @@ -719,4 +719,3 @@ - diff --git a/ui/suite/canvas/canvas.js b/ui/suite/canvas/canvas.js new file mode 100644 index 0000000..23b5a3a --- /dev/null +++ b/ui/suite/canvas/canvas.js @@ -0,0 +1,1120 @@ +/* ============================================================================= + CANVAS MODULE - Whiteboard/Drawing Application + ============================================================================= */ + +(function () { + "use strict"; + + // ============================================================================= + // STATE + // ============================================================================= + + const state = { + canvasId: null, + canvasName: "Untitled Canvas", + tool: "select", + color: "#000000", + strokeWidth: 2, + fillColor: "transparent", + fontSize: 16, + fontFamily: "Inter", + zoom: 1, + panX: 0, + panY: 0, + isDrawing: false, + isPanning: false, + startX: 0, + startY: 0, + elements: [], + selectedElement: null, + clipboard: null, + history: [], + historyIndex: -1, + gridEnabled: true, + snapToGrid: true, + gridSize: 20, + }; + + let canvas = null; + let ctx = null; + + // ============================================================================= + // INITIALIZATION + // ============================================================================= + + function init() { + canvas = document.getElementById("canvas"); + if (!canvas) { + console.warn("Canvas element not found"); + return; + } + ctx = canvas.getContext("2d"); + + resizeCanvas(); + bindEvents(); + loadFromUrl(); + render(); + + console.log("Canvas module initialized"); + } + + function resizeCanvas() { + if (!canvas) return; + const container = canvas.parentElement; + if (container) { + canvas.width = container.clientWidth || 1200; + canvas.height = container.clientHeight || 800; + } + } + + function bindEvents() { + if (!canvas) return; + + canvas.addEventListener("mousedown", handleMouseDown); + canvas.addEventListener("mousemove", handleMouseMove); + canvas.addEventListener("mouseup", handleMouseUp); + canvas.addEventListener("mouseleave", handleMouseUp); + canvas.addEventListener("wheel", handleWheel); + canvas.addEventListener("dblclick", handleDoubleClick); + + document.addEventListener("keydown", handleKeyDown); + window.addEventListener("resize", () => { + resizeCanvas(); + render(); + }); + + // Touch support + canvas.addEventListener("touchstart", handleTouchStart); + canvas.addEventListener("touchmove", handleTouchMove); + canvas.addEventListener("touchend", handleTouchEnd); + } + + // ============================================================================= + // TOOL SELECTION + // ============================================================================= + + function selectTool(tool) { + state.tool = tool; + + // Update UI + document.querySelectorAll(".tool-btn").forEach((btn) => { + btn.classList.toggle("active", btn.dataset.tool === tool); + }); + + // Update cursor + const cursors = { + select: "default", + pan: "grab", + pencil: "crosshair", + brush: "crosshair", + eraser: "crosshair", + rectangle: "crosshair", + ellipse: "crosshair", + line: "crosshair", + arrow: "crosshair", + text: "text", + sticky: "crosshair", + image: "crosshair", + connector: "crosshair", + frame: "crosshair", + }; + canvas.style.cursor = cursors[tool] || "default"; + } + + // ============================================================================= + // MOUSE HANDLERS + // ============================================================================= + + function handleMouseDown(e) { + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX - rect.left - state.panX) / state.zoom; + const y = (e.clientY - rect.top - state.panY) / state.zoom; + + state.startX = x; + state.startY = y; + + if (state.tool === "pan") { + state.isPanning = true; + canvas.style.cursor = "grabbing"; + return; + } + + if (state.tool === "select") { + const element = findElementAt(x, y); + selectElement(element); + if (element) { + state.isDrawing = true; // For dragging + } + return; + } + + state.isDrawing = true; + + if (state.tool === "text") { + createTextElement(x, y); + state.isDrawing = false; + return; + } + + if (state.tool === "sticky") { + createStickyNote(x, y); + state.isDrawing = false; + return; + } + + if (state.tool === "pencil" || state.tool === "brush") { + const element = { + id: generateId(), + type: "path", + points: [{ x, y }], + color: state.color, + strokeWidth: + state.tool === "brush" ? state.strokeWidth * 3 : state.strokeWidth, + }; + state.elements.push(element); + state.selectedElement = element; + } + } + + function handleMouseMove(e) { + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX - rect.left - state.panX) / state.zoom; + const y = (e.clientY - rect.top - state.panY) / state.zoom; + + if (state.isPanning) { + state.panX += e.movementX; + state.panY += e.movementY; + render(); + return; + } + + if (!state.isDrawing) return; + + if (state.tool === "pencil" || state.tool === "brush") { + if (state.selectedElement && state.selectedElement.points) { + state.selectedElement.points.push({ x, y }); + render(); + } + return; + } + + if (state.tool === "eraser") { + const element = findElementAt(x, y); + if (element) { + state.elements = state.elements.filter((el) => el.id !== element.id); + render(); + } + return; + } + + if (state.tool === "select" && state.selectedElement) { + const dx = x - state.startX; + const dy = y - state.startY; + state.selectedElement.x += dx; + state.selectedElement.y += dy; + state.startX = x; + state.startY = y; + render(); + return; + } + + // Preview shape while drawing + render(); + drawPreviewShape(state.startX, state.startY, x, y); + } + + function handleMouseUp(e) { + if (state.isPanning) { + state.isPanning = false; + canvas.style.cursor = state.tool === "pan" ? "grab" : "default"; + return; + } + + if (!state.isDrawing) return; + + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX - rect.left - state.panX) / state.zoom; + const y = (e.clientY - rect.top - state.panY) / state.zoom; + + if ( + ["rectangle", "ellipse", "line", "arrow", "frame"].includes(state.tool) + ) { + const element = createShapeElement( + state.tool, + state.startX, + state.startY, + x, + y, + ); + state.elements.push(element); + saveToHistory(); + } + + if (state.tool === "pencil" || state.tool === "brush") { + saveToHistory(); + } + + state.isDrawing = false; + render(); + } + + function handleWheel(e) { + e.preventDefault(); + const delta = e.deltaY > 0 ? -0.1 : 0.1; + const newZoom = Math.max(0.1, Math.min(5, state.zoom + delta)); + state.zoom = newZoom; + updateZoomDisplay(); + render(); + } + + function handleDoubleClick(e) { + const rect = canvas.getBoundingClientRect(); + const x = (e.clientX - rect.left - state.panX) / state.zoom; + const y = (e.clientY - rect.top - state.panY) / state.zoom; + + const element = findElementAt(x, y); + if (element && element.type === "text") { + editTextElement(element); + } + } + + // ============================================================================= + // TOUCH HANDLERS + // ============================================================================= + + function handleTouchStart(e) { + e.preventDefault(); + const touch = e.touches[0]; + handleMouseDown({ clientX: touch.clientX, clientY: touch.clientY }); + } + + function handleTouchMove(e) { + e.preventDefault(); + const touch = e.touches[0]; + handleMouseMove({ + clientX: touch.clientX, + clientY: touch.clientY, + movementX: 0, + movementY: 0, + }); + } + + function handleTouchEnd(e) { + e.preventDefault(); + const touch = e.changedTouches[0]; + handleMouseUp({ clientX: touch.clientX, clientY: touch.clientY }); + } + + // ============================================================================= + // KEYBOARD HANDLERS + // ============================================================================= + + function handleKeyDown(e) { + const isMod = e.ctrlKey || e.metaKey; + + // Tool shortcuts + if (!isMod && !e.target.matches("input, textarea")) { + const toolKeys = { + v: "select", + h: "pan", + p: "pencil", + b: "brush", + e: "eraser", + r: "rectangle", + o: "ellipse", + l: "line", + a: "arrow", + t: "text", + s: "sticky", + i: "image", + c: "connector", + f: "frame", + }; + if (toolKeys[e.key.toLowerCase()]) { + selectTool(toolKeys[e.key.toLowerCase()]); + return; + } + } + + if (isMod && e.key === "z") { + e.preventDefault(); + if (e.shiftKey) { + redo(); + } else { + undo(); + } + } else if (isMod && e.key === "y") { + e.preventDefault(); + redo(); + } else if (isMod && e.key === "c") { + e.preventDefault(); + copyElement(); + } else if (isMod && e.key === "v") { + e.preventDefault(); + pasteElement(); + } else if (isMod && e.key === "x") { + e.preventDefault(); + cutElement(); + } else if (isMod && e.key === "a") { + e.preventDefault(); + selectAll(); + } else if (e.key === "Delete" || e.key === "Backspace") { + if (state.selectedElement && !e.target.matches("input, textarea")) { + e.preventDefault(); + deleteSelected(); + } + } else if (e.key === "Escape") { + selectElement(null); + } else if (e.key === "+" || e.key === "=") { + if (isMod) { + e.preventDefault(); + zoomIn(); + } + } else if (e.key === "-") { + if (isMod) { + e.preventDefault(); + zoomOut(); + } + } else if (e.key === "0" && isMod) { + e.preventDefault(); + resetZoom(); + } + } + + // ============================================================================= + // ELEMENT CREATION + // ============================================================================= + + function createShapeElement(type, x1, y1, x2, y2) { + const minX = Math.min(x1, x2); + const minY = Math.min(y1, y2); + const width = Math.abs(x2 - x1); + const height = Math.abs(y2 - y1); + + return { + id: generateId(), + type: type, + x: minX, + y: minY, + width: width, + height: height, + x1: x1, + y1: y1, + x2: x2, + y2: y2, + color: state.color, + fillColor: state.fillColor, + strokeWidth: state.strokeWidth, + }; + } + + function createTextElement(x, y) { + const text = prompt("Enter text:"); + if (!text) return; + + const element = { + id: generateId(), + type: "text", + x: x, + y: y, + text: text, + color: state.color, + fontSize: state.fontSize, + fontFamily: state.fontFamily, + }; + state.elements.push(element); + saveToHistory(); + render(); + } + + function createStickyNote(x, y) { + const element = { + id: generateId(), + type: "sticky", + x: x, + y: y, + width: 200, + height: 200, + text: "Double-click to edit", + color: "#ffeb3b", + }; + state.elements.push(element); + saveToHistory(); + render(); + } + + function editTextElement(element) { + const newText = prompt("Edit text:", element.text); + if (newText !== null) { + element.text = newText; + saveToHistory(); + render(); + } + } + + // ============================================================================= + // ELEMENT SELECTION & MANIPULATION + // ============================================================================= + + function findElementAt(x, y) { + for (let i = state.elements.length - 1; i >= 0; i--) { + const el = state.elements[i]; + if (isPointInElement(x, y, el)) { + return el; + } + } + return null; + } + + function isPointInElement(x, y, el) { + const margin = 5; + switch (el.type) { + case "rectangle": + case "frame": + case "sticky": + return ( + x >= el.x - margin && + x <= el.x + el.width + margin && + y >= el.y - margin && + y <= el.y + el.height + margin + ); + case "ellipse": + const cx = el.x + el.width / 2; + const cy = el.y + el.height / 2; + const rx = el.width / 2 + margin; + const ry = el.height / 2 + margin; + return (x - cx) ** 2 / rx ** 2 + (y - cy) ** 2 / ry ** 2 <= 1; + case "text": + return ( + x >= el.x - margin && + x <= el.x + 200 && + y >= el.y - el.fontSize && + y <= el.y + margin + ); + case "line": + case "arrow": + return ( + distanceToLine(x, y, el.x1, el.y1, el.x2, el.y2) <= + margin + el.strokeWidth + ); + case "path": + if (!el.points) return false; + for (const pt of el.points) { + if (Math.abs(pt.x - x) < margin && Math.abs(pt.y - y) < margin) { + return true; + } + } + return false; + default: + return false; + } + } + + function distanceToLine(x, y, x1, y1, x2, y2) { + const A = x - x1; + const B = y - y1; + const C = x2 - x1; + const D = y2 - y1; + const dot = A * C + B * D; + const lenSq = C * C + D * D; + let param = lenSq !== 0 ? dot / lenSq : -1; + let xx, yy; + if (param < 0) { + xx = x1; + yy = y1; + } else if (param > 1) { + xx = x2; + yy = y2; + } else { + xx = x1 + param * C; + yy = y1 + param * D; + } + const dx = x - xx; + const dy = y - yy; + return Math.sqrt(dx * dx + dy * dy); + } + + function selectElement(element) { + state.selectedElement = element; + render(); + } + + function selectAll() { + // Select all - for now just render all as selected + render(); + } + + function deleteSelected() { + if (!state.selectedElement) return; + state.elements = state.elements.filter( + (el) => el.id !== state.selectedElement.id, + ); + state.selectedElement = null; + saveToHistory(); + render(); + } + + function copyElement() { + if (state.selectedElement) { + state.clipboard = JSON.parse(JSON.stringify(state.selectedElement)); + } + } + + function cutElement() { + copyElement(); + deleteSelected(); + } + + function pasteElement() { + if (!state.clipboard) return; + const newElement = JSON.parse(JSON.stringify(state.clipboard)); + newElement.id = generateId(); + newElement.x = (newElement.x || 0) + 20; + newElement.y = (newElement.y || 0) + 20; + if (newElement.x1 !== undefined) { + newElement.x1 += 20; + newElement.y1 += 20; + newElement.x2 += 20; + newElement.y2 += 20; + } + state.elements.push(newElement); + state.selectedElement = newElement; + saveToHistory(); + render(); + } + + // ============================================================================= + // RENDERING + // ============================================================================= + + function render() { + if (!ctx || !canvas) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.save(); + ctx.translate(state.panX, state.panY); + ctx.scale(state.zoom, state.zoom); + + // Draw grid + if (state.gridEnabled) { + drawGrid(); + } + + // Draw elements + for (const element of state.elements) { + drawElement(element); + } + + // Draw selection + if (state.selectedElement) { + drawSelection(state.selectedElement); + } + + ctx.restore(); + } + + function drawGrid() { + const gridSize = state.gridSize; + const width = canvas.width / state.zoom; + const height = canvas.height / state.zoom; + + ctx.strokeStyle = "#e0e0e0"; + ctx.lineWidth = 0.5; + + for (let x = 0; x < width; x += gridSize) { + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + ctx.stroke(); + } + + for (let y = 0; y < height; y += gridSize) { + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(width, y); + ctx.stroke(); + } + } + + function drawElement(el) { + ctx.strokeStyle = el.color || state.color; + ctx.fillStyle = el.fillColor || "transparent"; + ctx.lineWidth = el.strokeWidth || state.strokeWidth; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + + switch (el.type) { + case "rectangle": + case "frame": + ctx.beginPath(); + ctx.rect(el.x, el.y, el.width, el.height); + if (el.fillColor && el.fillColor !== "transparent") { + ctx.fill(); + } + ctx.stroke(); + break; + + case "ellipse": + ctx.beginPath(); + ctx.ellipse( + el.x + el.width / 2, + el.y + el.height / 2, + el.width / 2, + el.height / 2, + 0, + 0, + Math.PI * 2, + ); + if (el.fillColor && el.fillColor !== "transparent") { + ctx.fill(); + } + ctx.stroke(); + break; + + case "line": + ctx.beginPath(); + ctx.moveTo(el.x1, el.y1); + ctx.lineTo(el.x2, el.y2); + ctx.stroke(); + break; + + case "arrow": + drawArrow(el.x1, el.y1, el.x2, el.y2); + break; + + case "path": + if (el.points && el.points.length > 0) { + ctx.beginPath(); + ctx.moveTo(el.points[0].x, el.points[0].y); + for (let i = 1; i < el.points.length; i++) { + ctx.lineTo(el.points[i].x, el.points[i].y); + } + ctx.stroke(); + } + break; + + case "text": + ctx.font = `${el.fontSize || 16}px ${el.fontFamily || "Inter"}`; + ctx.fillStyle = el.color || "#000000"; + ctx.fillText(el.text, el.x, el.y); + break; + + case "sticky": + ctx.fillStyle = el.color || "#ffeb3b"; + ctx.fillRect(el.x, el.y, el.width, el.height); + ctx.strokeStyle = "#c0a000"; + ctx.strokeRect(el.x, el.y, el.width, el.height); + ctx.fillStyle = "#000000"; + ctx.font = "14px Inter"; + wrapText(el.text, el.x + 10, el.y + 25, el.width - 20, 18); + break; + } + } + + function drawArrow(x1, y1, x2, y2) { + const headLength = 15; + const angle = Math.atan2(y2 - y1, x2 - x1); + + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(x2, y2); + ctx.lineTo( + x2 - headLength * Math.cos(angle - Math.PI / 6), + y2 - headLength * Math.sin(angle - Math.PI / 6), + ); + ctx.moveTo(x2, y2); + ctx.lineTo( + x2 - headLength * Math.cos(angle + Math.PI / 6), + y2 - headLength * Math.sin(angle + Math.PI / 6), + ); + ctx.stroke(); + } + + function drawPreviewShape(x1, y1, x2, y2) { + ctx.save(); + ctx.translate(state.panX, state.panY); + ctx.scale(state.zoom, state.zoom); + ctx.strokeStyle = state.color; + ctx.lineWidth = state.strokeWidth; + ctx.setLineDash([5, 5]); + + switch (state.tool) { + case "rectangle": + case "frame": + ctx.strokeRect( + Math.min(x1, x2), + Math.min(y1, y2), + Math.abs(x2 - x1), + Math.abs(y2 - y1), + ); + break; + case "ellipse": + ctx.beginPath(); + ctx.ellipse( + (x1 + x2) / 2, + (y1 + y2) / 2, + Math.abs(x2 - x1) / 2, + Math.abs(y2 - y1) / 2, + 0, + 0, + Math.PI * 2, + ); + ctx.stroke(); + break; + case "line": + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + break; + case "arrow": + drawArrow(x1, y1, x2, y2); + break; + } + + ctx.restore(); + } + + function drawSelection(el) { + ctx.strokeStyle = "#2196f3"; + ctx.lineWidth = 2 / state.zoom; + ctx.setLineDash([5 / state.zoom, 5 / state.zoom]); + + let x, y, w, h; + if (el.type === "line" || el.type === "arrow") { + x = Math.min(el.x1, el.x2) - 5; + y = Math.min(el.y1, el.y2) - 5; + w = Math.abs(el.x2 - el.x1) + 10; + h = Math.abs(el.y2 - el.y1) + 10; + } else if (el.type === "path") { + const bounds = getPathBounds(el.points); + x = bounds.minX - 5; + y = bounds.minY - 5; + w = bounds.maxX - bounds.minX + 10; + h = bounds.maxY - bounds.minY + 10; + } else { + x = el.x - 5; + y = el.y - 5; + w = (el.width || 100) + 10; + h = (el.height || 20) + 10; + } + + ctx.strokeRect(x, y, w, h); + ctx.setLineDash([]); + } + + function getPathBounds(points) { + if (!points || points.length === 0) { + return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; + } + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity; + for (const pt of points) { + minX = Math.min(minX, pt.x); + minY = Math.min(minY, pt.y); + maxX = Math.max(maxX, pt.x); + maxY = Math.max(maxY, pt.y); + } + return { minX, minY, maxX, maxY }; + } + + function wrapText(text, x, y, maxWidth, lineHeight) { + const words = text.split(" "); + let line = ""; + for (const word of words) { + const testLine = line + word + " "; + const metrics = ctx.measureText(testLine); + if (metrics.width > maxWidth && line !== "") { + ctx.fillText(line, x, y); + line = word + " "; + y += lineHeight; + } else { + line = testLine; + } + } + ctx.fillText(line, x, y); + } + + // ============================================================================= + // ZOOM CONTROLS + // ============================================================================= + + function zoomIn() { + state.zoom = Math.min(5, state.zoom + 0.1); + updateZoomDisplay(); + render(); + } + + function zoomOut() { + state.zoom = Math.max(0.1, state.zoom - 0.1); + updateZoomDisplay(); + render(); + } + + function resetZoom() { + state.zoom = 1; + state.panX = 0; + state.panY = 0; + updateZoomDisplay(); + render(); + } + + function fitToScreen() { + // Calculate bounds of all elements + if (state.elements.length === 0) { + resetZoom(); + return; + } + + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity; + for (const el of state.elements) { + if (el.x !== undefined) { + minX = Math.min(minX, el.x); + minY = Math.min(minY, el.y); + maxX = Math.max(maxX, el.x + (el.width || 100)); + maxY = Math.max(maxY, el.y + (el.height || 50)); + } + } + + const contentWidth = maxX - minX + 100; + const contentHeight = maxY - minY + 100; + const scaleX = canvas.width / contentWidth; + const scaleY = canvas.height / contentHeight; + state.zoom = Math.min(scaleX, scaleY, 1); + state.panX = -minX * state.zoom + 50; + state.panY = -minY * state.zoom + 50; + + updateZoomDisplay(); + render(); + } + + function updateZoomDisplay() { + const el = document.getElementById("zoom-level"); + if (el) { + el.textContent = Math.round(state.zoom * 100) + "%"; + } + } + + // ============================================================================= + // HISTORY (UNDO/REDO) + // ============================================================================= + + function saveToHistory() { + // Remove any redo states + state.history = state.history.slice(0, state.historyIndex + 1); + // Save current state + state.history.push(JSON.stringify(state.elements)); + state.historyIndex = state.history.length - 1; + // Limit history size + if (state.history.length > 50) { + state.history.shift(); + state.historyIndex--; + } + } + + function undo() { + if (state.historyIndex > 0) { + state.historyIndex--; + state.elements = JSON.parse(state.history[state.historyIndex]); + state.selectedElement = null; + render(); + } + } + + function redo() { + if (state.historyIndex < state.history.length - 1) { + state.historyIndex++; + state.elements = JSON.parse(state.history[state.historyIndex]); + state.selectedElement = null; + render(); + } + } + + // ============================================================================= + // CLEAR CANVAS + // ============================================================================= + + function clearCanvas() { + if (!confirm("Clear the entire canvas? This cannot be undone.")) return; + state.elements = []; + state.selectedElement = null; + saveToHistory(); + render(); + } + + // ============================================================================= + // COLOR & STYLE + // ============================================================================= + + function setColor(color) { + state.color = color; + if (state.selectedElement) { + state.selectedElement.color = color; + saveToHistory(); + render(); + } + } + + function setFillColor(color) { + state.fillColor = color; + if (state.selectedElement) { + state.selectedElement.fillColor = color; + saveToHistory(); + render(); + } + } + + function setStrokeWidth(width) { + state.strokeWidth = parseInt(width); + if (state.selectedElement) { + state.selectedElement.strokeWidth = state.strokeWidth; + saveToHistory(); + render(); + } + } + + function toggleGrid() { + state.gridEnabled = !state.gridEnabled; + render(); + } + + // ============================================================================= + // SAVE/LOAD + // ============================================================================= + + function loadFromUrl() { + const urlParams = new URLSearchParams(window.location.search); + const canvasId = urlParams.get("id"); + if (canvasId) { + loadCanvas(canvasId); + } + } + + async function loadCanvas(canvasId) { + try { + const response = await fetch(`/api/canvas/${canvasId}`); + if (response.ok) { + const data = await response.json(); + state.canvasId = canvasId; + state.canvasName = data.name || "Untitled Canvas"; + state.elements = data.elements || []; + saveToHistory(); + render(); + } + } catch (e) { + console.error("Failed to load canvas:", e); + } + } + + async function saveCanvas() { + try { + const response = await fetch( + "/api/canvas" + (state.canvasId ? `/${state.canvasId}` : ""), + { + method: state.canvasId ? "PUT" : "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + name: state.canvasName, + elements: state.elements, + }), + }, + ); + if (response.ok) { + const data = await response.json(); + if (data.id) { + state.canvasId = data.id; + window.history.replaceState({}, "", `?id=${state.canvasId}`); + } + showNotification("Canvas saved", "success"); + } + } catch (e) { + console.error("Failed to save canvas:", e); + showNotification("Failed to save canvas", "error"); + } + } + + function exportCanvas(format) { + if (format === "png" || format === "jpg") { + const dataUrl = canvas.toDataURL(`image/${format}`); + const link = document.createElement("a"); + link.download = `${state.canvasName}.${format}`; + link.href = dataUrl; + link.click(); + } else if (format === "json") { + const data = JSON.stringify( + { name: state.canvasName, elements: state.elements }, + null, + 2, + ); + const blob = new Blob([data], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.download = `${state.canvasName}.json`; + link.href = url; + link.click(); + URL.revokeObjectURL(url); + } + } + + // ============================================================================= + // UTILITIES + // ============================================================================= + + function generateId() { + return "el_" + Math.random().toString(36).substr(2, 9); + } + + 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("Canvas", message); + else if (type === "error") window.GBAlerts.error("Canvas", message); + else window.GBAlerts.info("Canvas", message); + } else { + console.log(`[${type}] ${message}`); + } + } + + // ============================================================================= + // EXPORT TO WINDOW + // ============================================================================= + + window.selectTool = selectTool; + window.zoomIn = zoomIn; + window.zoomOut = zoomOut; + window.resetZoom = resetZoom; + window.fitToScreen = fitToScreen; + window.undo = undo; + window.redo = redo; + window.clearCanvas = clearCanvas; + window.setColor = setColor; + window.setFillColor = setFillColor; + window.setStrokeWidth = setStrokeWidth; + window.toggleGrid = toggleGrid; + window.saveCanvas = saveCanvas; + window.exportCanvas = exportCanvas; + window.deleteSelected = deleteSelected; + window.copyElement = copyElement; + window.cutElement = cutElement; + window.pasteElement = pasteElement; + + // ============================================================================= + // INITIALIZE + // ============================================================================= + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/ui/suite/chat/chat.js b/ui/suite/chat/chat.js index 3c9eff2..2e3cd0c 100644 --- a/ui/suite/chat/chat.js +++ b/ui/suite/chat/chat.js @@ -740,6 +740,33 @@ function parseMarkdown(text) { .replace(/`([^`]+)`/gim, "$1") .replace(/\n/gim, "
"); } +// Export projector functions for onclick handlers in projector.html +window.openProjector = openProjector; +window.closeProjector = closeProjector; +window.closeProjectorOnOverlay = closeProjectorOnOverlay; +window.toggleFullscreen = toggleFullscreen; +window.downloadContent = downloadContent; +window.shareContent = shareContent; +window.togglePlayPause = togglePlayPause; +window.mediaSeekBack = mediaSeekBack; +window.mediaSeekForward = mediaSeekForward; +window.toggleMute = toggleMute; +window.setVolume = setVolume; +window.toggleLoop = toggleLoop; +window.prevSlide = prevSlide; +window.nextSlide = nextSlide; +window.goToSlide = goToSlide; +window.zoomIn = zoomIn; +window.zoomOut = zoomOut; +window.prevImage = prevImage; +window.nextImage = nextImage; +window.rotateImage = rotateImage; +window.fitToScreen = fitToScreen; +window.toggleLineNumbers = toggleLineNumbers; +window.toggleWordWrap = toggleWordWrap; +window.setCodeTheme = setCodeTheme; +window.copyCode = copyCode; + if (window.htmx) { htmx.on("htmx:wsMessage", function (event) { try { diff --git a/ui/suite/dashboards/dashboards.js b/ui/suite/dashboards/dashboards.js new file mode 100644 index 0000000..6f51173 --- /dev/null +++ b/ui/suite/dashboards/dashboards.js @@ -0,0 +1,744 @@ +/* ============================================================================= + DASHBOARDS MODULE - Business Intelligence Dashboards + ============================================================================= */ + +(function () { + "use strict"; + + // ============================================================================= + // STATE + // ============================================================================= + + const state = { + dashboards: [], + dataSources: [], + currentDashboard: null, + selectedWidgetType: null, + isEditing: false, + widgets: [], + filters: { + category: "all", + search: "", + }, + }; + + // ============================================================================= + // INITIALIZATION + // ============================================================================= + + function init() { + loadDashboards(); + loadDataSources(); + bindEvents(); + console.log("Dashboards module initialized"); + } + + function bindEvents() { + // Search input + const searchInput = document.getElementById("dashboard-search"); + if (searchInput) { + searchInput.addEventListener("input", (e) => { + state.filters.search = e.target.value; + filterDashboards(); + }); + } + + // Category filter + const categorySelect = document.getElementById("category-filter"); + if (categorySelect) { + categorySelect.addEventListener("change", (e) => { + state.filters.category = e.target.value; + filterDashboards(); + }); + } + + // Dashboard card clicks + document.addEventListener("click", (e) => { + const card = e.target.closest(".dashboard-card"); + if (card && !e.target.closest("button")) { + const dashboardId = card.dataset.id; + if (dashboardId) { + openDashboard(dashboardId); + } + } + }); + } + + // ============================================================================= + // DASHBOARD CRUD + // ============================================================================= + + async function loadDashboards() { + try { + const response = await fetch("/api/dashboards"); + if (response.ok) { + const data = await response.json(); + state.dashboards = data.dashboards || []; + renderDashboardList(); + } + } catch (e) { + console.error("Failed to load dashboards:", e); + } + } + + function renderDashboardList() { + const container = document.getElementById("dashboards-grid"); + if (!container) return; + + const filtered = state.dashboards.filter((d) => { + const matchesSearch = + !state.filters.search || + d.name.toLowerCase().includes(state.filters.search.toLowerCase()); + const matchesCategory = + state.filters.category === "all" || + d.category === state.filters.category; + return matchesSearch && matchesCategory; + }); + + if (filtered.length === 0) { + container.innerHTML = ` +
+ πŸ“Š +

No dashboards found

+

Create your first dashboard to visualize your data

+ +
+ `; + return; + } + + container.innerHTML = filtered + .map( + (dashboard) => ` +
+
+
πŸ“Š
+
+
+

${escapeHtml(dashboard.name)}

+

${escapeHtml(dashboard.description || "No description")}

+
+ ${escapeHtml(dashboard.category || "General")} + Updated ${formatRelativeTime(dashboard.updated_at)} +
+
+
+ + + +
+
+ `, + ) + .join(""); + } + + function filterDashboards() { + renderDashboardList(); + } + + // ============================================================================= + // CREATE DASHBOARD MODAL + // ============================================================================= + + function showCreateDashboardModal() { + const modal = document.getElementById("createDashboardModal"); + if (modal) { + modal.style.display = "flex"; + // Reset form + const form = modal.querySelector("form"); + if (form) form.reset(); + } + } + + function closeCreateDashboardModal() { + const modal = document.getElementById("createDashboardModal"); + if (modal) { + modal.style.display = "none"; + } + } + + async function createDashboard(formData) { + const data = { + name: formData.get("name") || "Untitled Dashboard", + description: formData.get("description") || "", + category: formData.get("category") || "general", + is_public: formData.get("is_public") === "on", + }; + + try { + const response = await fetch("/api/dashboards", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + + if (response.ok) { + const result = await response.json(); + showNotification("Dashboard created", "success"); + closeCreateDashboardModal(); + loadDashboards(); + + // Open the new dashboard for editing + if (result.id) { + openDashboard(result.id); + } + } else { + showNotification("Failed to create dashboard", "error"); + } + } catch (e) { + console.error("Failed to create dashboard:", e); + showNotification("Failed to create dashboard", "error"); + } + } + + // Handle form submission + document.addEventListener("submit", (e) => { + if (e.target.id === "createDashboardForm") { + e.preventDefault(); + createDashboard(new FormData(e.target)); + } else if (e.target.id === "addDataSourceForm") { + e.preventDefault(); + addDataSource(new FormData(e.target)); + } else if (e.target.id === "addWidgetForm") { + e.preventDefault(); + addWidget(new FormData(e.target)); + } + }); + + // ============================================================================= + // DASHBOARD VIEWER + // ============================================================================= + + async function openDashboard(dashboardId) { + try { + const response = await fetch(`/api/dashboards/${dashboardId}`); + if (response.ok) { + const dashboard = await response.json(); + state.currentDashboard = dashboard; + state.widgets = dashboard.widgets || []; + showDashboardViewer(dashboard); + } + } catch (e) { + console.error("Failed to open dashboard:", e); + showNotification("Failed to load dashboard", "error"); + } + } + + function showDashboardViewer(dashboard) { + const viewer = document.getElementById("dashboard-viewer"); + const list = document.getElementById("dashboards-list"); + + if (viewer) viewer.classList.remove("hidden"); + if (list) list.classList.add("hidden"); + + // Update title + const titleEl = document.getElementById("viewer-dashboard-name"); + if (titleEl) titleEl.textContent = dashboard.name; + + // Render widgets + renderWidgets(dashboard.widgets || []); + } + + function closeDashboardViewer() { + const viewer = document.getElementById("dashboard-viewer"); + const list = document.getElementById("dashboards-list"); + + if (viewer) viewer.classList.add("hidden"); + if (list) list.classList.remove("hidden"); + + state.currentDashboard = null; + state.isEditing = false; + } + + function renderWidgets(widgets) { + const container = document.getElementById("widgets-grid"); + if (!container) return; + + if (widgets.length === 0) { + container.innerHTML = ` +
+ πŸ“ˆ +

No widgets yet

+

Add widgets to visualize your data

+ +
+ `; + return; + } + + container.innerHTML = widgets + .map( + (widget) => ` +
+
+

${escapeHtml(widget.title)}

+
+ + +
+
+
+ ${renderWidgetContent(widget)} +
+
+ `, + ) + .join(""); + } + + function renderWidgetContent(widget) { + // Placeholder rendering - in production, this would render actual charts + const icons = { + line_chart: "πŸ“ˆ", + bar_chart: "πŸ“Š", + pie_chart: "πŸ₯§", + area_chart: "πŸ“‰", + scatter_plot: "⚬", + kpi: "🎯", + table: "πŸ“‹", + gauge: "⏲️", + map: "πŸ—ΊοΈ", + text: "πŸ“", + }; + + return ` +
+ ${icons[widget.widget_type] || "πŸ“Š"} + ${widget.widget_type} +
+ `; + } + + // ============================================================================= + // DASHBOARD ACTIONS + // ============================================================================= + + async function refreshDashboard() { + if (!state.currentDashboard) return; + showNotification("Refreshing dashboard...", "info"); + await openDashboard(state.currentDashboard.id); + showNotification("Dashboard refreshed", "success"); + } + + function editDashboard() { + if (!state.currentDashboard) return; + state.isEditing = true; + + const viewer = document.getElementById("dashboard-viewer"); + if (viewer) { + viewer.classList.add("editing"); + } + + showNotification("Edit mode enabled", "info"); + } + + function editDashboardById(dashboardId) { + openDashboard(dashboardId).then(() => { + editDashboard(); + }); + } + + function shareDashboard() { + if (!state.currentDashboard) return; + + const shareUrl = `${window.location.origin}/dashboards/${state.currentDashboard.id}`; + navigator.clipboard.writeText(shareUrl).then(() => { + showNotification("Share link copied to clipboard", "success"); + }); + } + + function exportDashboard() { + if (!state.currentDashboard) return; + + // Export as JSON + const data = JSON.stringify(state.currentDashboard, null, 2); + const blob = new Blob([data], { type: "application/json" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.download = `${state.currentDashboard.name}.json`; + link.href = url; + link.click(); + URL.revokeObjectURL(url); + + showNotification("Dashboard exported", "success"); + } + + async function duplicateDashboard(dashboardId) { + try { + const response = await fetch(`/api/dashboards/${dashboardId}/duplicate`, { + method: "POST", + }); + + if (response.ok) { + showNotification("Dashboard duplicated", "success"); + loadDashboards(); + } else { + showNotification("Failed to duplicate dashboard", "error"); + } + } catch (e) { + console.error("Failed to duplicate dashboard:", e); + showNotification("Failed to duplicate dashboard", "error"); + } + } + + async function deleteDashboard(dashboardId) { + if (!confirm("Delete this dashboard? This cannot be undone.")) return; + + try { + const response = await fetch(`/api/dashboards/${dashboardId}`, { + method: "DELETE", + }); + + if (response.ok) { + showNotification("Dashboard deleted", "success"); + loadDashboards(); + if (state.currentDashboard?.id === dashboardId) { + closeDashboardViewer(); + } + } else { + showNotification("Failed to delete dashboard", "error"); + } + } catch (e) { + console.error("Failed to delete dashboard:", e); + showNotification("Failed to delete dashboard", "error"); + } + } + + // ============================================================================= + // DATA SOURCES + // ============================================================================= + + async function loadDataSources() { + try { + const response = await fetch("/api/dashboards/data-sources"); + if (response.ok) { + const data = await response.json(); + state.dataSources = data.data_sources || []; + renderDataSourcesList(); + } + } catch (e) { + console.error("Failed to load data sources:", e); + } + } + + function renderDataSourcesList() { + const container = document.getElementById("data-sources-list"); + if (!container) return; + + if (state.dataSources.length === 0) { + container.innerHTML = ` +

No data sources configured

+ `; + return; + } + + container.innerHTML = state.dataSources + .map( + (source) => ` +
+ ${getSourceIcon(source.source_type)} +
+ ${escapeHtml(source.name)} + ${source.source_type} +
+ +
+ `, + ) + .join(""); + } + + function getSourceIcon(sourceType) { + const icons = { + postgresql: "🐘", + mysql: "🐬", + api: "πŸ”Œ", + csv: "πŸ“„", + json: "πŸ“‹", + elasticsearch: "πŸ”", + mongodb: "πŸƒ", + }; + return icons[sourceType] || "πŸ“Š"; + } + + function showAddDataSourceModal() { + const modal = document.getElementById("addDataSourceModal"); + if (modal) { + modal.style.display = "flex"; + const form = modal.querySelector("form"); + if (form) form.reset(); + } + } + + function closeAddDataSourceModal() { + const modal = document.getElementById("addDataSourceModal"); + if (modal) { + modal.style.display = "none"; + } + } + + async function testDataSourceConnection() { + const form = document.getElementById("addDataSourceForm"); + if (!form) return; + + const formData = new FormData(form); + showNotification("Testing connection...", "info"); + + try { + const response = await fetch("/api/dashboards/data-sources/test", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + source_type: formData.get("source_type"), + connection_string: formData.get("connection_string"), + }), + }); + + if (response.ok) { + showNotification("Connection successful!", "success"); + } else { + showNotification("Connection failed", "error"); + } + } catch (e) { + showNotification("Connection test failed", "error"); + } + } + + async function addDataSource(formData) { + const data = { + name: formData.get("name"), + source_type: formData.get("source_type"), + connection_string: formData.get("connection_string"), + }; + + try { + const response = await fetch("/api/dashboards/data-sources", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + + if (response.ok) { + showNotification("Data source added", "success"); + closeAddDataSourceModal(); + loadDataSources(); + } else { + showNotification("Failed to add data source", "error"); + } + } catch (e) { + console.error("Failed to add data source:", e); + showNotification("Failed to add data source", "error"); + } + } + + async function removeDataSource(sourceId) { + if (!confirm("Remove this data source?")) return; + + try { + const response = await fetch(`/api/dashboards/data-sources/${sourceId}`, { + method: "DELETE", + }); + + if (response.ok) { + showNotification("Data source removed", "success"); + loadDataSources(); + } + } catch (e) { + console.error("Failed to remove data source:", e); + } + } + + // ============================================================================= + // WIDGETS + // ============================================================================= + + function showAddWidgetModal() { + const modal = document.getElementById("addWidgetModal"); + if (modal) { + modal.style.display = "flex"; + state.selectedWidgetType = null; + updateWidgetTypeSelection(); + } + } + + function closeAddWidgetModal() { + const modal = document.getElementById("addWidgetModal"); + if (modal) { + modal.style.display = "none"; + } + } + + function selectWidgetType(widgetType) { + state.selectedWidgetType = widgetType; + updateWidgetTypeSelection(); + } + + function updateWidgetTypeSelection() { + document.querySelectorAll(".widget-option").forEach((btn) => { + btn.classList.toggle( + "selected", + btn.dataset.type === state.selectedWidgetType, + ); + }); + + // Show/hide configuration section + const configSection = document.getElementById("widget-config-section"); + if (configSection) { + configSection.style.display = state.selectedWidgetType ? "block" : "none"; + } + } + + async function addWidget(formData) { + if (!state.currentDashboard || !state.selectedWidgetType) { + showNotification("Please select a widget type", "error"); + return; + } + + const data = { + dashboard_id: state.currentDashboard.id, + widget_type: state.selectedWidgetType, + title: formData.get("widget_title") || "Untitled Widget", + data_source_id: formData.get("data_source_id"), + config: { + width: parseInt(formData.get("width")) || 1, + height: parseInt(formData.get("height")) || 1, + }, + }; + + try { + const response = await fetch( + `/api/dashboards/${state.currentDashboard.id}/widgets`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }, + ); + + if (response.ok) { + showNotification("Widget added", "success"); + closeAddWidgetModal(); + openDashboard(state.currentDashboard.id); + } else { + showNotification("Failed to add widget", "error"); + } + } catch (e) { + console.error("Failed to add widget:", e); + showNotification("Failed to add widget", "error"); + } + } + + function editWidget(widgetId) { + // TODO: Implement widget editing modal + showNotification("Widget editing coming soon", "info"); + } + + async function removeWidget(widgetId) { + if (!confirm("Remove this widget?")) return; + + try { + const response = await fetch(`/api/dashboards/widgets/${widgetId}`, { + method: "DELETE", + }); + + if (response.ok) { + showNotification("Widget removed", "success"); + if (state.currentDashboard) { + openDashboard(state.currentDashboard.id); + } + } + } catch (e) { + console.error("Failed to remove widget:", e); + } + } + + // ============================================================================= + // UTILITIES + // ============================================================================= + + function escapeHtml(text) { + if (!text) return ""; + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + function formatRelativeTime(dateString) { + if (!dateString) return "Never"; + try { + const date = new Date(dateString); + const now = new Date(); + const diffMs = now - date; + const diffMin = Math.floor(diffMs / 60000); + const diffHour = Math.floor(diffMin / 60); + const diffDay = Math.floor(diffHour / 24); + + if (diffMin < 1) return "just now"; + if (diffMin < 60) return `${diffMin}m ago`; + if (diffHour < 24) return `${diffHour}h ago`; + if (diffDay < 7) return `${diffDay}d ago`; + return date.toLocaleDateString(); + } catch { + return dateString; + } + } + + 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("Dashboards", message); + else if (type === "error") window.GBAlerts.error("Dashboards", message); + else window.GBAlerts.info("Dashboards", message); + } else { + console.log(`[${type}] ${message}`); + } + } + + // ============================================================================= + // EXPORT TO WINDOW + // ============================================================================= + + // Create Dashboard Modal + window.showCreateDashboardModal = showCreateDashboardModal; + window.closeCreateDashboardModal = closeCreateDashboardModal; + + // Dashboard Viewer + window.openDashboard = openDashboard; + window.closeDashboardViewer = closeDashboardViewer; + window.refreshDashboard = refreshDashboard; + window.editDashboard = editDashboard; + window.editDashboardById = editDashboardById; + window.shareDashboard = shareDashboard; + window.exportDashboard = exportDashboard; + window.duplicateDashboard = duplicateDashboard; + window.deleteDashboard = deleteDashboard; + + // Data Sources + window.showAddDataSourceModal = showAddDataSourceModal; + window.closeAddDataSourceModal = closeAddDataSourceModal; + window.testDataSourceConnection = testDataSourceConnection; + window.removeDataSource = removeDataSource; + + // Widgets + window.showAddWidgetModal = showAddWidgetModal; + window.closeAddWidgetModal = closeAddWidgetModal; + window.selectWidgetType = selectWidgetType; + window.editWidget = editWidget; + window.removeWidget = removeWidget; + + // ============================================================================= + // INITIALIZE + // ============================================================================= + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/ui/suite/drive/drive.js b/ui/suite/drive/drive.js index 9413b1d..17b74e1 100644 --- a/ui/suite/drive/drive.js +++ b/ui/suite/drive/drive.js @@ -1214,6 +1214,140 @@ loadFiles(currentPath, currentBucket); } + // ============================================================================= + // MISSING FUNCTIONS FOR HTML ONCLICK HANDLERS + // ============================================================================= + + function toggleView(type) { + setView(type); + } + + function setView(type) { + const gridBtn = document.getElementById("grid-view-btn"); + const listBtn = document.getElementById("list-view-btn"); + const fileGrid = document.getElementById("file-grid"); + const fileList = document.getElementById("file-list"); + const fileView = document.getElementById("file-view"); + + if (type === "grid") { + gridBtn?.classList.add("active"); + listBtn?.classList.remove("active"); + if (fileGrid) fileGrid.style.display = "grid"; + if (fileList) fileList.style.display = "none"; + if (fileView) fileView.className = "file-grid"; + } else { + gridBtn?.classList.remove("active"); + listBtn?.classList.add("active"); + if (fileGrid) fileGrid.style.display = "none"; + if (fileList) fileList.style.display = "block"; + if (fileView) fileView.className = "file-list"; + } + } + + function openFolder(el) { + const path = + el?.dataset?.path || el?.querySelector(".file-name")?.textContent; + if (path) { + currentPath = path.startsWith("/") ? path : currentPath + "/" + path; + loadFiles(currentPath); + } + } + + function selectFile(el) { + const path = el?.dataset?.path; + if (path) { + toggleSelection(path); + el.classList.toggle("selected", selectedFiles.has(path)); + } else { + // Toggle visual selection + document.querySelectorAll(".file-item.selected").forEach((item) => { + if (item !== el) item.classList.remove("selected"); + }); + el.classList.toggle("selected"); + } + updateSelectionUI(); + } + + function setActiveNav(el) { + document.querySelectorAll(".nav-item").forEach((item) => { + item.classList.remove("active"); + }); + el.classList.add("active"); + } + + function toggleInfoPanel() { + const panel = + document.getElementById("info-panel") || + document.getElementById("details-panel"); + if (panel) { + panel.classList.toggle("open"); + panel.classList.toggle("hidden"); + } + } + + function toggleAIPanel() { + const panel = + document.getElementById("ai-panel") || + document.querySelector(".ai-panel"); + if (panel) { + panel.classList.toggle("open"); + } + } + + function aiAction(action) { + const messages = { + organize: + "I'll help you organize your files. What folder would you like to organize?", + find: "What file are you looking for?", + analyze: "Select a file and I'll analyze its contents.", + share: "Select files to share. Who would you like to share with?", + }; + addAIMessage("assistant", messages[action] || "How can I help you?"); + } + + function sendAIMessage() { + const input = document.getElementById("ai-input"); + if (!input || !input.value.trim()) return; + + const message = input.value.trim(); + input.value = ""; + + addAIMessage("user", message); + // Simulate AI response + setTimeout(() => { + addAIMessage("assistant", `Processing your request: "${message}"`); + }, 500); + } + + function addAIMessage(type, content) { + const container = + document.getElementById("ai-messages") || + document.querySelector(".ai-messages"); + if (!container) return; + + const div = document.createElement("div"); + div.className = `ai-message ${type}`; + div.innerHTML = `
${content}
`; + container.appendChild(div); + container.scrollTop = container.scrollHeight; + } + + function updateSelectionUI() { + const count = selectedFiles.size; + const bulkActions = document.getElementById("bulk-actions"); + if (bulkActions) { + bulkActions.style.display = count > 0 ? "flex" : "none"; + } + const countEl = document.getElementById("selection-count"); + if (countEl) { + countEl.textContent = `${count} selected`; + } + } + + function uploadFile() { + triggerUpload(); + } + window.DriveModule = { init, loadFiles, @@ -1238,6 +1372,18 @@ navigateUp, }; + // Export functions for HTML onclick handlers + window.toggleView = toggleView; + window.setView = setView; + window.openFolder = openFolder; + window.selectFile = selectFile; + window.setActiveNav = setActiveNav; + window.toggleInfoPanel = toggleInfoPanel; + window.toggleAIPanel = toggleAIPanel; + window.aiAction = aiAction; + window.sendAIMessage = sendAIMessage; + window.uploadFile = uploadFile; + if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { diff --git a/ui/suite/goals/goals.js b/ui/suite/goals/goals.js new file mode 100644 index 0000000..bd25566 --- /dev/null +++ b/ui/suite/goals/goals.js @@ -0,0 +1,445 @@ +/* ============================================================================= + GOALS/OKR MODULE - Objectives & Key Results + ============================================================================= */ + +(function () { + "use strict"; + + // ============================================================================= + // STATE + // ============================================================================= + + const state = { + currentView: "dashboard", + objectives: [], + selectedObjective: null, + filters: { + status: "all", + owner: "all", + period: "current", + }, + }; + + // ============================================================================= + // INITIALIZATION + // ============================================================================= + + function init() { + loadObjectives(); + bindEvents(); + console.log("Goals module initialized"); + } + + function bindEvents() { + // View toggle buttons + document.querySelectorAll(".view-btn").forEach((btn) => { + btn.addEventListener("click", function () { + const view = this.dataset.view; + if (view) { + switchGoalsView(view); + } + }); + }); + + // Objective cards + document.addEventListener("click", (e) => { + const card = e.target.closest(".objective-card"); + if (card) { + const objectiveId = card.dataset.id; + if (objectiveId) { + selectObjective(objectiveId); + } + } + }); + } + + // ============================================================================= + // VIEW SWITCHING + // ============================================================================= + + function switchGoalsView(view) { + state.currentView = view; + + // Update button states + document.querySelectorAll(".view-btn").forEach((btn) => { + btn.classList.toggle("active", btn.dataset.view === view); + }); + + // Update view panels + document.querySelectorAll(".goals-view").forEach((panel) => { + panel.classList.toggle("active", panel.id === `${view}-view`); + }); + + // Load view-specific data if using HTMX + if (typeof htmx !== "undefined") { + const viewContainer = document.getElementById("goals-content"); + if (viewContainer) { + htmx.ajax("GET", `/api/ui/goals/${view}`, { target: viewContainer }); + } + } + } + + // ============================================================================= + // DETAILS PANEL + // ============================================================================= + + function toggleGoalsPanel() { + const panel = document.getElementById("details-panel"); + if (panel) { + panel.classList.toggle("collapsed"); + } + } + + function openGoalsPanel() { + const panel = document.getElementById("details-panel"); + if (panel) { + panel.classList.remove("collapsed"); + } + } + + function closeGoalsPanel() { + const panel = document.getElementById("details-panel"); + if (panel) { + panel.classList.add("collapsed"); + } + } + + // ============================================================================= + // OBJECTIVES + // ============================================================================= + + async function loadObjectives() { + try { + const response = await fetch("/api/goals/objectives"); + if (response.ok) { + const data = await response.json(); + state.objectives = data.objectives || []; + renderObjectives(); + } + } catch (e) { + console.error("Failed to load objectives:", e); + } + } + + function renderObjectives() { + const container = document.getElementById("objectives-list"); + if (!container) return; + + if (state.objectives.length === 0) { + container.innerHTML = ` +
+ 🎯 +

No objectives yet

+

Create your first objective to start tracking goals

+ +
+ `; + return; + } + + container.innerHTML = state.objectives + .map( + (obj) => ` +
+
+

${escapeHtml(obj.title)}

+ ${obj.status} +
+
+
+
+
+ ${obj.progress || 0}% +
+
+ ${escapeHtml(obj.owner_name || "Unassigned")} + ${formatDate(obj.end_date)} +
+
+ `, + ) + .join(""); + } + + function selectObjective(objectiveId) { + const objective = state.objectives.find((o) => o.id === objectiveId); + if (!objective) return; + + state.selectedObjective = objective; + renderObjectives(); + renderObjectiveDetails(objective); + openGoalsPanel(); + } + + function renderObjectiveDetails(objective) { + const container = document.getElementById("objective-details"); + if (!container) return; + + container.innerHTML = ` +
+

${escapeHtml(objective.title)}

+
+ + +
+
+
+
+ + ${objective.status} +
+
+ +
+
+
+ ${objective.progress || 0}% +
+
+ +

${escapeHtml(objective.description || "No description")}

+
+
+ +

${escapeHtml(objective.owner_name || "Unassigned")}

+
+
+ +

${formatDate(objective.start_date)} - ${formatDate(objective.end_date)}

+
+
+ +
+ ${renderKeyResults(objective.key_results || [])} +
+ +
+
+ `; + } + + function renderKeyResults(keyResults) { + if (keyResults.length === 0) { + return '

No key results defined

'; + } + + return keyResults + .map( + (kr) => ` +
+
+ ${escapeHtml(kr.title)} + ${kr.current_value || 0} / ${kr.target_value || 100} +
+
+
+
+
+ `, + ) + .join(""); + } + + // ============================================================================= + // CRUD OPERATIONS + // ============================================================================= + + function showCreateObjectiveModal() { + const modal = document.getElementById("create-objective-modal"); + if (modal) { + if (modal.showModal) { + modal.showModal(); + } else { + modal.classList.add("open"); + } + } else { + // Fallback: create a simple prompt-based flow + const title = prompt("Enter objective title:"); + if (title) { + createObjective({ title }); + } + } + } + + function closeCreateObjectiveModal() { + const modal = document.getElementById("create-objective-modal"); + if (modal) { + if (modal.close) { + modal.close(); + } else { + modal.classList.remove("open"); + } + } + } + + async function createObjective(data) { + try { + const response = await fetch("/api/goals/objectives", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (response.ok) { + showNotification("Objective created", "success"); + loadObjectives(); + closeCreateObjectiveModal(); + } else { + showNotification("Failed to create objective", "error"); + } + } catch (e) { + console.error("Failed to create objective:", e); + showNotification("Failed to create objective", "error"); + } + } + + function editObjective(objectiveId) { + const objective = state.objectives.find((o) => o.id === objectiveId); + if (!objective) return; + + const newTitle = prompt("Edit objective title:", objective.title); + if (newTitle && newTitle !== objective.title) { + updateObjective(objectiveId, { title: newTitle }); + } + } + + async function updateObjective(objectiveId, data) { + try { + const response = await fetch(`/api/goals/objectives/${objectiveId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }); + if (response.ok) { + showNotification("Objective updated", "success"); + loadObjectives(); + } else { + showNotification("Failed to update objective", "error"); + } + } catch (e) { + console.error("Failed to update objective:", e); + showNotification("Failed to update objective", "error"); + } + } + + async function deleteObjective(objectiveId) { + if (!confirm("Delete this objective? This cannot be undone.")) return; + + try { + const response = await fetch(`/api/goals/objectives/${objectiveId}`, { + method: "DELETE", + }); + if (response.ok) { + showNotification("Objective deleted", "success"); + state.selectedObjective = null; + closeGoalsPanel(); + loadObjectives(); + } else { + showNotification("Failed to delete objective", "error"); + } + } catch (e) { + console.error("Failed to delete objective:", e); + showNotification("Failed to delete objective", "error"); + } + } + + function addKeyResult(objectiveId) { + const title = prompt("Enter key result title:"); + if (!title) return; + + const targetValue = prompt("Enter target value:", "100"); + if (!targetValue) return; + + createKeyResult(objectiveId, { + title, + target_value: parseFloat(targetValue) || 100, + current_value: 0, + }); + } + + async function createKeyResult(objectiveId, data) { + try { + const response = await fetch( + `/api/goals/objectives/${objectiveId}/key-results`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(data), + }, + ); + if (response.ok) { + showNotification("Key result added", "success"); + loadObjectives(); + } else { + showNotification("Failed to add key result", "error"); + } + } catch (e) { + console.error("Failed to create key result:", e); + showNotification("Failed to add key result", "error"); + } + } + + // ============================================================================= + // UTILITIES + // ============================================================================= + + function escapeHtml(text) { + if (!text) return ""; + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; + } + + function formatDate(dateString) { + if (!dateString) return "Not set"; + try { + const date = new Date(dateString); + return date.toLocaleDateString(); + } catch { + return dateString; + } + } + + 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("Goals", message); + else if (type === "error") window.GBAlerts.error("Goals", message); + else window.GBAlerts.info("Goals", message); + } else { + console.log(`[${type}] ${message}`); + } + } + + // ============================================================================= + // EXPORT TO WINDOW + // ============================================================================= + + window.switchGoalsView = switchGoalsView; + window.toggleGoalsPanel = toggleGoalsPanel; + window.openGoalsPanel = openGoalsPanel; + window.closeGoalsPanel = closeGoalsPanel; + window.selectObjective = selectObjective; + window.showCreateObjectiveModal = showCreateObjectiveModal; + window.closeCreateObjectiveModal = closeCreateObjectiveModal; + window.createObjective = createObjective; + window.editObjective = editObjective; + window.updateObjective = updateObjective; + window.deleteObjective = deleteObjective; + window.addKeyResult = addKeyResult; + + // ============================================================================= + // INITIALIZE + // ============================================================================= + + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +})(); diff --git a/ui/suite/index.html b/ui/suite/index.html index 9872ae2..ff0298e 100644 --- a/ui/suite/index.html +++ b/ui/suite/index.html @@ -2376,6 +2376,10 @@ + + + +