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>
|
||||
|
||||
<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(/\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 {
|
||||
|
|
|
|||
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);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 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
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="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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue