Fix UI button handlers and add missing JS modules

- Add admin-functions.js with 40+ button handlers
- Load admin scripts in main suite/index.html
- Fix slides.html gbSlides -> window.slidesApp
- Add canvas.js, dashboards.js, goals.js modules
- Export missing functions in drive.js, chat.js
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-13 14:49:22 -03:00
parent e3b5929b99
commit eb785b9a69
10 changed files with 3226 additions and 6 deletions

View file

@ -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;
})();

View file

@ -719,4 +719,3 @@
</dialog>
<link rel="stylesheet" href="admin/admin.css" />
<script src="admin/admin.js"></script>

1120
ui/suite/canvas/canvas.js Normal file

File diff suppressed because it is too large Load diff

View file

@ -740,6 +740,33 @@ function parseMarkdown(text) {
.replace(/`([^`]+)`/gim, "<code>$1</code>")
.replace(/\n/gim, "<br>");
}
// 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 {

View file

@ -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 = `
<div class="empty-state">
<span class="empty-icon">📊</span>
<h3>No dashboards found</h3>
<p>Create your first dashboard to visualize your data</p>
<button class="btn-primary" onclick="showCreateDashboardModal()">
Create Dashboard
</button>
</div>
`;
return;
}
container.innerHTML = filtered
.map(
(dashboard) => `
<div class="dashboard-card" data-id="${dashboard.id}">
<div class="dashboard-preview">
<div class="preview-placeholder">📊</div>
</div>
<div class="dashboard-info">
<h3>${escapeHtml(dashboard.name)}</h3>
<p>${escapeHtml(dashboard.description || "No description")}</p>
<div class="dashboard-meta">
<span class="category">${escapeHtml(dashboard.category || "General")}</span>
<span class="updated">Updated ${formatRelativeTime(dashboard.updated_at)}</span>
</div>
</div>
<div class="dashboard-actions">
<button class="btn-icon" onclick="event.stopPropagation(); editDashboardById('${dashboard.id}')" title="Edit"></button>
<button class="btn-icon" onclick="event.stopPropagation(); duplicateDashboard('${dashboard.id}')" title="Duplicate">📋</button>
<button class="btn-icon" onclick="event.stopPropagation(); deleteDashboard('${dashboard.id}')" title="Delete">🗑</button>
</div>
</div>
`,
)
.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 = `
<div class="empty-widgets">
<span class="empty-icon">📈</span>
<h3>No widgets yet</h3>
<p>Add widgets to visualize your data</p>
<button class="btn-primary" onclick="showAddWidgetModal()">
Add Widget
</button>
</div>
`;
return;
}
container.innerHTML = widgets
.map(
(widget) => `
<div class="widget" data-id="${widget.id}" style="grid-column: span ${widget.width || 1}; grid-row: span ${widget.height || 1};">
<div class="widget-header">
<h4>${escapeHtml(widget.title)}</h4>
<div class="widget-actions">
<button class="btn-icon btn-sm" onclick="editWidget('${widget.id}')" title="Edit"></button>
<button class="btn-icon btn-sm" onclick="removeWidget('${widget.id}')" title="Remove"></button>
</div>
</div>
<div class="widget-content" id="widget-content-${widget.id}">
${renderWidgetContent(widget)}
</div>
</div>
`,
)
.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 `
<div class="widget-placeholder">
<span class="widget-icon">${icons[widget.widget_type] || "📊"}</span>
<span class="widget-type">${widget.widget_type}</span>
</div>
`;
}
// =============================================================================
// 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 = `
<p class="empty-message">No data sources configured</p>
`;
return;
}
container.innerHTML = state.dataSources
.map(
(source) => `
<div class="data-source-item" data-id="${source.id}">
<span class="source-icon">${getSourceIcon(source.source_type)}</span>
<div class="source-info">
<span class="source-name">${escapeHtml(source.name)}</span>
<span class="source-type">${source.source_type}</span>
</div>
<button class="btn-icon btn-sm" onclick="removeDataSource('${source.id}')" title="Remove"></button>
</div>
`,
)
.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();
}
})();

View file

@ -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 = `<div class="ai-message-bubble">${content}</div>`;
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 {

445
ui/suite/goals/goals.js Normal file
View file

@ -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 = `
<div class="empty-state">
<span class="empty-icon">🎯</span>
<h3>No objectives yet</h3>
<p>Create your first objective to start tracking goals</p>
<button class="btn-primary" onclick="showCreateObjectiveModal()">
Create Objective
</button>
</div>
`;
return;
}
container.innerHTML = state.objectives
.map(
(obj) => `
<div class="objective-card ${state.selectedObjective?.id === obj.id ? "selected" : ""}"
data-id="${obj.id}">
<div class="objective-header">
<h3>${escapeHtml(obj.title)}</h3>
<span class="status-badge ${obj.status}">${obj.status}</span>
</div>
<div class="objective-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${obj.progress || 0}%"></div>
</div>
<span class="progress-text">${obj.progress || 0}%</span>
</div>
<div class="objective-meta">
<span class="owner">${escapeHtml(obj.owner_name || "Unassigned")}</span>
<span class="due-date">${formatDate(obj.end_date)}</span>
</div>
</div>
`,
)
.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 = `
<div class="detail-header">
<h2>${escapeHtml(objective.title)}</h2>
<div class="detail-actions">
<button class="btn-icon" onclick="editObjective('${objective.id}')" title="Edit"></button>
<button class="btn-icon" onclick="deleteObjective('${objective.id}')" title="Delete">🗑</button>
</div>
</div>
<div class="detail-body">
<div class="detail-section">
<label>Status</label>
<span class="status-badge ${objective.status}">${objective.status}</span>
</div>
<div class="detail-section">
<label>Progress</label>
<div class="progress-bar large">
<div class="progress-fill" style="width: ${objective.progress || 0}%"></div>
</div>
<span>${objective.progress || 0}%</span>
</div>
<div class="detail-section">
<label>Description</label>
<p>${escapeHtml(objective.description || "No description")}</p>
</div>
<div class="detail-section">
<label>Owner</label>
<p>${escapeHtml(objective.owner_name || "Unassigned")}</p>
</div>
<div class="detail-section">
<label>Timeline</label>
<p>${formatDate(objective.start_date)} - ${formatDate(objective.end_date)}</p>
</div>
<div class="detail-section">
<label>Key Results</label>
<div id="key-results-list">
${renderKeyResults(objective.key_results || [])}
</div>
<button class="btn-secondary btn-sm" onclick="addKeyResult('${objective.id}')">
+ Add Key Result
</button>
</div>
</div>
`;
}
function renderKeyResults(keyResults) {
if (keyResults.length === 0) {
return '<p class="empty-message">No key results defined</p>';
}
return keyResults
.map(
(kr) => `
<div class="key-result-item">
<div class="kr-header">
<span class="kr-title">${escapeHtml(kr.title)}</span>
<span class="kr-progress">${kr.current_value || 0} / ${kr.target_value || 100}</span>
</div>
<div class="progress-bar small">
<div class="progress-fill" style="width: ${((kr.current_value || 0) / (kr.target_value || 100)) * 100}%"></div>
</div>
</div>
`,
)
.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();
}
})();

View file

@ -2376,6 +2376,10 @@
<script src="js/base.js"></script>
<script src="tasks/tasks.js?v=20260102"></script>
<!-- Admin module functions (button handlers for HTMX-loaded admin views) -->
<script src="admin/admin.js"></script>
<script src="admin/admin-functions.js"></script>
<!-- Application initialization -->
<script>
// Simple initialization for HTMX app

View file

@ -630,7 +630,7 @@
<div class="modal-actions">
<button
class="btn-secondary"
onclick="gbSlides.hideModal('imageModal')"
onclick="window.slidesApp.hideModal('imageModal')"
>
Cancel
</button>
@ -781,7 +781,7 @@
<div class="modal-actions">
<button
class="btn-secondary"
onclick="gbSlides.hideModal('notesModal')"
onclick="window.slidesApp.hideModal('notesModal')"
>
Cancel
</button>
@ -815,7 +815,7 @@
<div class="modal-actions">
<button
class="btn-secondary"
onclick="gbSlides.hideModal('backgroundModal')"
onclick="window.slidesApp.hideModal('backgroundModal')"
>
Cancel
</button>

View file

@ -1060,7 +1060,7 @@
}
} else if (e.key === "Escape") {
clearSelection();
hideContextMenus();
hideAllContextMenus();
if (state.isPresenting) {
exitPresentation();
}
@ -1706,6 +1706,14 @@
elements.slideContextMenu?.classList.add("hidden");
}
function showSlideContextMenu(e, slideIndex) {
e.preventDefault();
e.stopPropagation();
state.currentSlideIndex = slideIndex;
hideAllContextMenus();
showContextMenu(elements.slideContextMenu, e.clientX, e.clientY);
}
function showContextMenu(menu, x, y) {
if (!menu) return;
menu.style.left = `${x}px`;
@ -2865,7 +2873,7 @@
updateMasterPreview();
}
window.gbSlides = {
window.slidesApp = {
init,
addSlide,
addTextBox,
@ -2885,6 +2893,7 @@
showSlideSorter,
exportToPdf,
showMasterSlideModal,
showSlideContextMenu,
};
if (document.readyState === "loading") {