SaaS/public/dashboard.js
Claude Code d1b5b72c46 🚀 Transform into full SaaS Automation Platform
Major Update: From simple click counter to comprehensive automation platform

## New Features:
 Full user authentication (register/login/logout)
 SQLite database with user management
 API credentials management system
 Workflow templates library (4 ready-to-use templates)
 User workflow management
 Comprehensive dashboard interface
 Detailed n8n integration instructions
 Security features (bcrypt, sessions, helmet)
 Automated testing suite (14 passing tests)

## Technical Stack:
- Backend: Node.js + Express + SQLite
- Frontend: Vanilla JS + Modern CSS
- Security: bcrypt, express-session, helmet
- Database: SQLite with proper schemas and indexes
- Testing: Mocha + Chai + Supertest

## Templates Included:
1. 📱 Telegram Bot Notifications
2. 📧 Email to Slack Integration
3. 💾 Google Drive Backup Automation
4. 📊 Lead Scoring Automation

## Access Points:
- Legacy app: http://localhost:3000/
- SaaS Platform: http://localhost:3000/dashboard
- API endpoints: /api/*

## Demo Credentials:
- Email: demo@example.com
- Password: demo123

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 11:05:29 +01:00

560 lines
No EOL
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function() {
// Состояние приложения
let currentUser = null;
let currentSection = 'overview';
// DOM элементы
const elements = {
loadingScreen: document.getElementById('loadingScreen'),
loginForm: document.getElementById('loginForm'),
registerForm: document.getElementById('registerForm'),
dashboard: document.getElementById('dashboard'),
// Auth forms
login: document.getElementById('login'),
register: document.getElementById('register'),
showRegister: document.getElementById('showRegister'),
showLogin: document.getElementById('showLogin'),
// Dashboard
userInfo: document.getElementById('userInfo'),
logoutBtn: document.getElementById('logoutBtn'),
navItems: document.querySelectorAll('.nav-item'),
sections: document.querySelectorAll('.section'),
// Stats
totalWorkflows: document.getElementById('totalWorkflows'),
totalCredentials: document.getElementById('totalCredentials'),
totalExecutions: document.getElementById('totalExecutions'),
// Lists
templatesGrid: document.getElementById('templatesGrid'),
workflowsList: document.getElementById('workflowsList'),
credentialsList: document.getElementById('credentialsList'),
// Filters
categoryFilter: document.getElementById('categoryFilter'),
featuredFilter: document.getElementById('featuredFilter'),
// Buttons
addCredentialBtn: document.getElementById('addCredentialBtn'),
createWorkflowBtn: document.getElementById('createWorkflowBtn'),
// Modal
credentialModal: document.getElementById('credentialModal'),
credentialForm: document.getElementById('credentialForm'),
cancelCredential: document.getElementById('cancelCredential'),
closeModal: document.querySelector('.close')
};
// Утилиты
function showElement(element) {
if (element) element.style.display = 'block';
}
function hideElement(element) {
if (element) element.style.display = 'none';
}
function showMessage(message, type = 'success') {
// Создаем элемент сообщения
const messageEl = document.createElement('div');
messageEl.className = `message ${type}`;
messageEl.textContent = message;
// Добавляем в начало dashboard-main
const main = document.querySelector('.dashboard-main');
if (main) {
main.insertBefore(messageEl, main.firstChild);
// Удаляем через 5 секунд
setTimeout(() => {
messageEl.remove();
}, 5000);
}
}
async function apiCall(url, options = {}) {
try {
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Network error');
}
return data;
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
// Инициализация приложения
async function init() {
try {
// Проверяем сессию
const userData = await apiCall('/api/user/profile');
currentUser = userData.user;
showDashboard();
} catch (error) {
// Пользователь не авторизован
showLogin();
} finally {
hideElement(elements.loadingScreen);
}
}
function showLogin() {
hideElement(elements.registerForm);
hideElement(elements.dashboard);
showElement(elements.loginForm);
}
function showRegister() {
hideElement(elements.loginForm);
hideElement(elements.dashboard);
showElement(elements.registerForm);
}
function showDashboard() {
hideElement(elements.loginForm);
hideElement(elements.registerForm);
showElement(elements.dashboard);
if (currentUser) {
elements.userInfo.textContent = `${currentUser.username} (${currentUser.subscription_type})`;
}
loadDashboardData();
}
// Переключение разделов
function switchSection(sectionName) {
currentSection = sectionName;
// Обновляем навигацию
elements.navItems.forEach(item => {
item.classList.toggle('active', item.dataset.section === sectionName);
});
// Показываем нужный раздел
elements.sections.forEach(section => {
section.classList.toggle('active', section.id === sectionName + 'Section');
});
// Загружаем данные для раздела
loadSectionData(sectionName);
}
async function loadDashboardData() {
try {
const stats = await apiCall('/api/user/stats');
elements.totalWorkflows.textContent = stats.stats.total_workflows;
elements.totalCredentials.textContent = stats.stats.total_credentials;
elements.totalExecutions.textContent = stats.stats.total_executions;
} catch (error) {
console.error('Failed to load dashboard data:', error);
}
}
async function loadSectionData(section) {
switch (section) {
case 'templates':
loadTemplates();
break;
case 'workflows':
loadWorkflows();
break;
case 'credentials':
loadCredentials();
break;
}
}
async function loadTemplates() {
try {
const category = elements.categoryFilter.value;
const featured = elements.featuredFilter.checked;
let url = '/api/templates?';
if (category) url += `category=${category}&`;
if (featured) url += 'featured=true&';
const data = await apiCall(url);
renderTemplates(data.templates);
} catch (error) {
console.error('Failed to load templates:', error);
elements.templatesGrid.innerHTML = '<div class="list-empty">Ошибка загрузки шаблонов</div>';
}
}
function renderTemplates(templates) {
if (templates.length === 0) {
elements.templatesGrid.innerHTML = '<div class="list-empty">Шаблоны не найдены</div>';
return;
}
elements.templatesGrid.innerHTML = templates.map(template => `
<div class="template-card">
<div class="template-header">
<h3 class="template-title">${template.name}</h3>
${template.is_featured ? '<span class="template-badge">Рекомендуем</span>' : ''}
</div>
<p class="template-description">${template.description}</p>
<div class="template-meta">
<span>📂 ${template.category || 'Общее'}</span>
<span>⏱️ ${template.estimated_setup_time || 15} мин</span>
<span>📊 ${template.difficulty_level || 'beginner'}</span>
</div>
<div class="template-actions">
<button class="btn btn-primary btn-small" onclick="viewTemplate(${template.id})">
Посмотреть
</button>
<button class="btn btn-secondary btn-small" onclick="useTemplate(${template.id})">
Использовать
</button>
</div>
</div>
`).join('');
}
async function loadWorkflows() {
try {
const data = await apiCall('/api/user/workflows');
renderWorkflows(data.workflows);
} catch (error) {
console.error('Failed to load workflows:', error);
elements.workflowsList.innerHTML = '<div class="list-empty">Ошибка загрузки процессов</div>';
}
}
function renderWorkflows(workflows) {
if (workflows.length === 0) {
elements.workflowsList.innerHTML = `
<div class="list-empty">
<p>У вас пока нет рабочих процессов</p>
<button class="btn btn-primary" onclick="switchSection('templates')">
Посмотреть шаблоны
</button>
</div>
`;
return;
}
elements.workflowsList.innerHTML = workflows.map(workflow => `
<div class="list-item">
<div class="list-item-content">
<div class="list-item-title">${workflow.workflow_name}</div>
<div class="list-item-meta">
Статус: ${workflow.status}
Создан: ${new Date(workflow.created_at).toLocaleDateString('ru-RU')}
Запусков: ${workflow.total_runs}
</div>
</div>
<div class="list-item-actions">
<button class="btn btn-primary btn-small" onclick="editWorkflow(${workflow.id})">
Редактировать
</button>
<button class="btn btn-secondary btn-small" onclick="deleteWorkflow(${workflow.id})">
Удалить
</button>
</div>
</div>
`).join('');
}
async function loadCredentials() {
try {
const data = await apiCall('/api/user/credentials');
renderCredentials(data.credentials);
} catch (error) {
console.error('Failed to load credentials:', error);
elements.credentialsList.innerHTML = '<div class="list-empty">Ошибка загрузки креденшелов</div>';
}
}
function renderCredentials(credentials) {
if (credentials.length === 0) {
elements.credentialsList.innerHTML = `
<div class="list-empty">
<p>У вас пока нет сохраненных API ключей</p>
<button class="btn btn-primary" onclick="showCredentialModal()">
Добавить первый ключ
</button>
</div>
`;
return;
}
elements.credentialsList.innerHTML = credentials.map(credential => `
<div class="list-item">
<div class="list-item-content">
<div class="list-item-title">${credential.service_name} (${credential.credential_type})</div>
<div class="list-item-meta">
${credential.description || 'Без описания'}
Добавлен: ${new Date(credential.created_at).toLocaleDateString('ru-RU')}
</div>
</div>
<div class="list-item-actions">
<button class="btn btn-secondary btn-small" onclick="deleteCredential(${credential.id})">
Удалить
</button>
</div>
</div>
`).join('');
}
// Модальные окна
function showCredentialModal() {
showElement(elements.credentialModal);
}
function hideCredentialModal() {
hideElement(elements.credentialModal);
elements.credentialForm.reset();
}
// Глобальные функции для кнопок
window.viewTemplate = async function(templateId) {
try {
const data = await apiCall(`/api/templates/${templateId}`);
const template = data.template;
// Показываем JSON шаблона для копирования
const jsonStr = JSON.stringify(template.template_data, null, 2);
const modal = document.createElement('div');
modal.className = 'modal';
modal.style.display = 'block';
modal.innerHTML = `
<div class="modal-content">
<span class="close">&times;</span>
<h3>📋 ${template.name}</h3>
<p>${template.description}</p>
<h4>Требуемые креденшелы:</h4>
<ul>
${template.required_credentials.map(cred => `<li>${cred}</li>`).join('')}
</ul>
<h4>JSON для n8n:</h4>
<textarea readonly style="width: 100%; height: 200px; font-family: monospace;">${jsonStr}</textarea>
<div class="modal-actions">
<button class="btn btn-primary" onclick="copyToClipboard('${jsonStr.replace(/'/g, "\\'").replace(/"/g, '\\"')}')">
📋 Копировать JSON
</button>
<button class="btn btn-secondary" onclick="this.closest('.modal').remove()">
Закрыть
</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.querySelector('.close').onclick = () => modal.remove();
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
} catch (error) {
showMessage('Ошибка загрузки шаблона: ' + error.message, 'error');
}
};
window.useTemplate = async function(templateId) {
try {
const workflowName = prompt('Введите название для вашего процесса:');
if (!workflowName) return;
await apiCall('/api/user/workflows', {
method: 'POST',
body: JSON.stringify({
template_id: templateId,
workflow_name: workflowName
})
});
showMessage('Рабочий процесс создан успешно!');
switchSection('workflows');
} catch (error) {
showMessage('Ошибка создания процесса: ' + error.message, 'error');
}
};
window.deleteCredential = async function(credentialId) {
if (!confirm('Вы уверены, что хотите удалить этот API ключ?')) return;
try {
await apiCall(`/api/user/credentials/${credentialId}`, {
method: 'DELETE'
});
showMessage('API ключ удален');
loadCredentials();
} catch (error) {
showMessage('Ошибка удаления: ' + error.message, 'error');
}
};
window.editWorkflow = function(workflowId) {
showMessage('Функция редактирования в разработке', 'warning');
};
window.deleteWorkflow = function(workflowId) {
showMessage('Функция удаления в разработке', 'warning');
};
window.copyToClipboard = function(text) {
navigator.clipboard.writeText(text.replace(/\\"/g, '"').replace(/\\'/g, "'")).then(() => {
showMessage('JSON скопирован в буфер обмена!');
}).catch(() => {
showMessage('Ошибка копирования', 'error');
});
};
// Обработчики событий
elements.showRegister.onclick = (e) => {
e.preventDefault();
showRegister();
};
elements.showLogin.onclick = (e) => {
e.preventDefault();
showLogin();
};
elements.login.onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {
email: formData.get('email') || document.getElementById('loginEmail').value,
password: formData.get('password') || document.getElementById('loginPassword').value
};
try {
const response = await apiCall('/api/auth/login', {
method: 'POST',
body: JSON.stringify(data)
});
currentUser = response.user;
showDashboard();
} catch (error) {
showMessage('Ошибка входа: ' + error.message, 'error');
}
};
elements.register.onsubmit = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = {
username: formData.get('username') || document.getElementById('registerUsername').value,
email: formData.get('email') || document.getElementById('registerEmail').value,
password: formData.get('password') || document.getElementById('registerPassword').value
};
try {
const response = await apiCall('/api/auth/register', {
method: 'POST',
body: JSON.stringify(data)
});
currentUser = response.user;
showDashboard();
} catch (error) {
showMessage('Ошибка регистрации: ' + error.message, 'error');
}
};
elements.logoutBtn.onclick = async () => {
try {
await apiCall('/api/auth/logout', { method: 'POST' });
currentUser = null;
showLogin();
} catch (error) {
showMessage('Ошибка выхода: ' + error.message, 'error');
}
};
// Навигация
elements.navItems.forEach(item => {
item.onclick = (e) => {
e.preventDefault();
switchSection(item.dataset.section);
};
});
// Action buttons
document.addEventListener('click', (e) => {
if (e.target.dataset.action === 'goto-credentials') {
switchSection('credentials');
} else if (e.target.dataset.action === 'goto-templates') {
switchSection('templates');
} else if (e.target.dataset.action === 'goto-instructions') {
switchSection('instructions');
}
});
// Фильтры шаблонов
elements.categoryFilter.onchange = () => {
if (currentSection === 'templates') {
loadTemplates();
}
};
elements.featuredFilter.onchange = () => {
if (currentSection === 'templates') {
loadTemplates();
}
};
// Модальные окна
elements.addCredentialBtn.onclick = showCredentialModal;
elements.cancelCredential.onclick = hideCredentialModal;
elements.closeModal.onclick = hideCredentialModal;
elements.credentialModal.onclick = (e) => {
if (e.target === elements.credentialModal) {
hideCredentialModal();
}
};
elements.credentialForm.onsubmit = async (e) => {
e.preventDefault();
const data = {
service_name: document.getElementById('serviceName').value,
credential_type: document.getElementById('credentialType').value,
value: document.getElementById('credentialValue').value,
description: document.getElementById('credentialDescription').value
};
try {
await apiCall('/api/user/credentials', {
method: 'POST',
body: JSON.stringify(data)
});
showMessage('API ключ сохранен успешно!');
hideCredentialModal();
loadCredentials();
} catch (error) {
showMessage('Ошибка сохранения: ' + error.message, 'error');
}
};
// Запуск приложения
init();
})();