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:
parent
e3b5929b99
commit
eb785b9a69
10 changed files with 3226 additions and 6 deletions
726
ui/suite/admin/admin-functions.js
Normal file
726
ui/suite/admin/admin-functions.js
Normal 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;
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
@ -719,4 +719,3 @@
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<link rel="stylesheet" href="admin/admin.css" />
|
<link rel="stylesheet" href="admin/admin.css" />
|
||||||
<script src="admin/admin.js"></script>
|
|
||||||
|
|
|
||||||
1120
ui/suite/canvas/canvas.js
Normal file
1120
ui/suite/canvas/canvas.js
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -740,6 +740,33 @@ function parseMarkdown(text) {
|
||||||
.replace(/`([^`]+)`/gim, "<code>$1</code>")
|
.replace(/`([^`]+)`/gim, "<code>$1</code>")
|
||||||
.replace(/\n/gim, "<br>");
|
.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) {
|
if (window.htmx) {
|
||||||
htmx.on("htmx:wsMessage", function (event) {
|
htmx.on("htmx:wsMessage", function (event) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
744
ui/suite/dashboards/dashboards.js
Normal file
744
ui/suite/dashboards/dashboards.js
Normal 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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -1214,6 +1214,140 @@
|
||||||
loadFiles(currentPath, currentBucket);
|
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 = {
|
window.DriveModule = {
|
||||||
init,
|
init,
|
||||||
loadFiles,
|
loadFiles,
|
||||||
|
|
@ -1238,6 +1372,18 @@
|
||||||
navigateUp,
|
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") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", init);
|
document.addEventListener("DOMContentLoaded", init);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
445
ui/suite/goals/goals.js
Normal file
445
ui/suite/goals/goals.js
Normal 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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -2376,6 +2376,10 @@
|
||||||
<script src="js/base.js"></script>
|
<script src="js/base.js"></script>
|
||||||
<script src="tasks/tasks.js?v=20260102"></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 -->
|
<!-- Application initialization -->
|
||||||
<script>
|
<script>
|
||||||
// Simple initialization for HTMX app
|
// Simple initialization for HTMX app
|
||||||
|
|
|
||||||
|
|
@ -630,7 +630,7 @@
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button
|
<button
|
||||||
class="btn-secondary"
|
class="btn-secondary"
|
||||||
onclick="gbSlides.hideModal('imageModal')"
|
onclick="window.slidesApp.hideModal('imageModal')"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -781,7 +781,7 @@
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button
|
<button
|
||||||
class="btn-secondary"
|
class="btn-secondary"
|
||||||
onclick="gbSlides.hideModal('notesModal')"
|
onclick="window.slidesApp.hideModal('notesModal')"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -815,7 +815,7 @@
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button
|
<button
|
||||||
class="btn-secondary"
|
class="btn-secondary"
|
||||||
onclick="gbSlides.hideModal('backgroundModal')"
|
onclick="window.slidesApp.hideModal('backgroundModal')"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -1060,7 +1060,7 @@
|
||||||
}
|
}
|
||||||
} else if (e.key === "Escape") {
|
} else if (e.key === "Escape") {
|
||||||
clearSelection();
|
clearSelection();
|
||||||
hideContextMenus();
|
hideAllContextMenus();
|
||||||
if (state.isPresenting) {
|
if (state.isPresenting) {
|
||||||
exitPresentation();
|
exitPresentation();
|
||||||
}
|
}
|
||||||
|
|
@ -1706,6 +1706,14 @@
|
||||||
elements.slideContextMenu?.classList.add("hidden");
|
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) {
|
function showContextMenu(menu, x, y) {
|
||||||
if (!menu) return;
|
if (!menu) return;
|
||||||
menu.style.left = `${x}px`;
|
menu.style.left = `${x}px`;
|
||||||
|
|
@ -2865,7 +2873,7 @@
|
||||||
updateMasterPreview();
|
updateMasterPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
window.gbSlides = {
|
window.slidesApp = {
|
||||||
init,
|
init,
|
||||||
addSlide,
|
addSlide,
|
||||||
addTextBox,
|
addTextBox,
|
||||||
|
|
@ -2885,6 +2893,7 @@
|
||||||
showSlideSorter,
|
showSlideSorter,
|
||||||
exportToPdf,
|
exportToPdf,
|
||||||
showMasterSlideModal,
|
showMasterSlideModal,
|
||||||
|
showSlideContextMenu,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue