/** * FormationsUIManager - Handles DOM updates and formation card rendering */ class FormationsUIManager { constructor() { this.targetEl = null; this.drawingControlsEl = null; this.nameInputEl = null; this.onDeleteFormation = null; this.onEditFormation = null; } /** * Initializes the UI elements. * @param {string} targetId - ID of the formations list container */ initUI(targetId) { this.targetEl = document.getElementById(targetId); if (!this.targetEl) { console.warn(`Formations container "${targetId}" not found.`); } this.drawingControlsEl = document.getElementById('formation_drawing_controls'); this.nameInputEl = document.getElementById('formation_name_input'); } /** * Register callback for delete formation. * @param {Function} callback - Function to call when delete is clicked */ registerDeleteCallback(callback) { this.onDeleteFormation = callback; } /** * Register callback for edit formation. * @param {Function} callback - Function to call when edit is clicked */ registerEditCallback(callback) { this.onEditFormation = callback; } /** * Show drawing controls. */ showDrawingControls() { if (this.drawingControlsEl) { this.drawingControlsEl.style.display = 'block'; } if (this.nameInputEl) { this.nameInputEl.value = ''; this.nameInputEl.focus(); } } /** * Hide drawing controls. */ hideDrawingControls() { if (this.drawingControlsEl) { this.drawingControlsEl.style.display = 'none'; } } /** * Get the formation name from input. * @returns {string} Formation name */ getFormationName() { return this.nameInputEl ? this.nameInputEl.value.trim() : ''; } /** * Render all formations as cards. * @param {Array} formations - List of formation objects */ renderFormations(formations) { if (!this.targetEl) { console.warn("Formations container not initialized"); return; } this.targetEl.innerHTML = ''; if (!formations || formations.length === 0) { this.targetEl.innerHTML = '

No formations yet. Click a button above to draw one.

'; return; } formations.forEach(formation => { const card = this._createFormationCard(formation); this.targetEl.appendChild(card); }); } /** * Create a formation card element. * @param {Object} formation - Formation data * @returns {HTMLElement} Card element */ _createFormationCard(formation) { const card = document.createElement('div'); card.className = 'formation-item'; card.dataset.tblKey = formation.tbl_key; card.dataset.type = formation.formation_type; // Format type for display const typeDisplay = this._formatType(formation.formation_type); card.innerHTML = `
${this._escapeHtml(formation.name)} ${typeDisplay}
${this._escapeHtml(formation.name)}
Type: ${typeDisplay} Color Scope: ${formation.exchange}/${formation.market}/${formation.timeframe}
`; // Add click handlers const deleteBtn = card.querySelector('.delete-button'); deleteBtn.addEventListener('click', (e) => { e.stopPropagation(); if (this.onDeleteFormation) { this.onDeleteFormation(formation.tbl_key, formation.name); } }); const editBtn = card.querySelector('.edit-button'); editBtn.addEventListener('click', (e) => { e.stopPropagation(); if (this.onEditFormation) { this.onEditFormation(formation.tbl_key); } }); // Card click to select card.addEventListener('click', () => { this._selectCard(card); if (this.onEditFormation) { this.onEditFormation(formation.tbl_key); } }); return card; } /** * Select a card visually. * @param {HTMLElement} card - Card to select */ _selectCard(card) { // Deselect all this.targetEl.querySelectorAll('.formation-item').forEach(c => { c.classList.remove('selected'); }); // Select this one card.classList.add('selected'); } /** * Format formation type for display. * @param {string} type - Formation type * @returns {string} Formatted type */ _formatType(type) { const typeMap = { 'support_resistance': 'Line', 'channel': 'Channel', 'triangle': 'Triangle', 'head_shoulders': 'H&S', 'double_bottom': 'Double Bottom', 'double_top': 'Double Top' }; return typeMap[type] || type; } /** * Escape HTML to prevent XSS. * @param {string} str - String to escape * @returns {string} Escaped string */ _escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } } /** * FormationsDataManager - Manages in-memory formations data */ class FormationsDataManager { constructor() { this.formations = []; } /** * Set all formations. * @param {Array} formations - List of formations */ setFormations(formations) { this.formations = Array.isArray(formations) ? formations : []; } /** * Get all formations. * @returns {Array} List of formations */ getAllFormations() { return this.formations; } /** * Add a new formation. * @param {Object} formation - Formation to add */ addFormation(formation) { if (formation) { this.formations.push(formation); } } /** * Update an existing formation. * @param {Object} updatedFormation - Updated formation data */ updateFormation(updatedFormation) { const index = this.formations.findIndex(f => f.tbl_key === updatedFormation.tbl_key); if (index !== -1) { this.formations[index] = { ...this.formations[index], ...updatedFormation }; } } /** * Remove a formation by tbl_key. * @param {string} tblKey - Formation tbl_key to remove */ removeFormation(tblKey) { this.formations = this.formations.filter(f => f.tbl_key !== tblKey); } /** * Get a formation by tbl_key. * @param {string} tblKey - Formation tbl_key * @returns {Object|null} Formation or null */ getFormation(tblKey) { return this.formations.find(f => f.tbl_key === tblKey) || null; } } /** * Formations - Main coordinator class for formations feature */ class Formations { constructor(ui) { this.ui = ui; this.comms = ui?.data?.comms; this.data = ui?.data; this.dataManager = new FormationsDataManager(); this.uiManager = new FormationsUIManager(); this.overlay = null; // Current drawing state this.drawingMode = null; this.currentScope = null; // Set up callbacks this.uiManager.registerDeleteCallback(this.deleteFormation.bind(this)); this.uiManager.registerEditCallback(this.selectFormation.bind(this)); this._initialized = false; } /** * Initialize the formations system. * @param {string} targetId - ID of formations list container */ initialize(targetId) { try { this.uiManager.initUI(targetId); if (!this.comms) { console.error("Communications instance not available for Formations"); return; } // Register socket handlers this.registerSocketHandlers(); // Get current scope from chart data this.currentScope = { exchange: this.data?.exchange || window.bt_data?.exchange || 'kucoin', market: this.data?.trading_pair || window.bt_data?.trading_pair || 'BTC/USDT', timeframe: this.data?.timeframe || window.bt_data?.timeframe || '1h' }; // Fetch formations for current scope this.fetchFormations(); this._initialized = true; console.log("Formations initialized for scope:", this.currentScope); } catch (error) { console.error("Error initializing Formations:", error); } } /** * Initialize the SVG overlay for drawing. * @param {string} chartContainerId - ID of chart container * @param {Object} chart - Lightweight Charts instance * @param {Object} candleSeries - Candlestick series for coordinate conversion */ initOverlay(chartContainerId, chart, candleSeries) { if (typeof FormationOverlay !== 'undefined') { this.overlay = new FormationOverlay(chartContainerId, chart, candleSeries); this.overlay.setOnSaveCallback(this.saveFormation.bind(this)); console.log("Formation overlay initialized"); } else { console.warn("FormationOverlay class not loaded"); } } /** * Register socket handlers for formations. */ registerSocketHandlers() { this.comms.on('formations', this.handleFormationsResponse.bind(this)); this.comms.on('formation_created', this.handleFormationCreated.bind(this)); this.comms.on('formation_updated', this.handleFormationUpdated.bind(this)); this.comms.on('formation_deleted', this.handleFormationDeleted.bind(this)); this.comms.on('formation_error', this.handleFormationError.bind(this)); } /** * Fetch formations for current scope. */ fetchFormations() { if (!this.comms || !this.currentScope) return; this.comms.sendToApp('request_formations', this.currentScope); } // ================ Socket Handlers ================ /** * Handle formations list response. * @param {Object} data - Response with formations array */ handleFormationsResponse(data) { console.log("Received formations:", data); const formations = data.formations || []; this.dataManager.setFormations(formations); this.uiManager.renderFormations(formations); // Render on overlay if available if (this.overlay) { this.overlay.clearAllFormations(); formations.forEach(f => this.overlay.renderFormation(f)); } } /** * Handle formation created event. * @param {Object} data - Response with formation data */ handleFormationCreated(data) { console.log("Formation created:", data); if (data.success && data.formation) { this.dataManager.addFormation(data.formation); this.uiManager.renderFormations(this.dataManager.getAllFormations()); if (this.overlay) { this.overlay.renderFormation(data.formation); } this.uiManager.hideDrawingControls(); this.drawingMode = null; } else { alert(`Failed to create formation: ${data.message}`); } } /** * Handle formation updated event. * @param {Object} data - Response with updated formation data */ handleFormationUpdated(data) { console.log("Formation updated:", data); if (data.success && data.formation) { this.dataManager.updateFormation(data.formation); this.uiManager.renderFormations(this.dataManager.getAllFormations()); if (this.overlay) { this.overlay.updateFormation(data.formation); } } else { alert(`Failed to update formation: ${data.message}`); } } /** * Handle formation deleted event. * @param {Object} data - Response with tbl_key of deleted formation */ handleFormationDeleted(data) { console.log("Formation deleted:", data); if (data.success && data.tbl_key) { this.dataManager.removeFormation(data.tbl_key); this.uiManager.renderFormations(this.dataManager.getAllFormations()); if (this.overlay) { this.overlay.removeFormation(data.tbl_key); } } } /** * Handle formation error. * @param {Object} data - Error data */ handleFormationError(data) { console.error("Formation error:", data.message); alert(`Formation error: ${data.message}`); } // ================ Drawing Methods ================ /** * Start drawing a new formation. * @param {string} type - Formation type ('support_resistance', 'channel') */ startDrawing(type) { console.log("Starting drawing mode:", type); this.drawingMode = type; // Show drawing controls this.uiManager.showDrawingControls(); // Tell overlay to start drawing if (this.overlay) { this.overlay.startDrawing(type); } } /** * Complete the current drawing. */ completeDrawing() { const name = this.uiManager.getFormationName(); if (!name) { alert("Please enter a formation name"); return; } if (this.overlay) { this.overlay.completeDrawing(name); } } /** * Cancel the current drawing. */ cancelDrawing() { this.drawingMode = null; this.uiManager.hideDrawingControls(); if (this.overlay) { this.overlay.cancelDrawing(); } } /** * Save a formation (callback from overlay). * @param {Object} formationData - Formation data to save */ saveFormation(formationData) { if (!this.comms) return; const payload = { ...formationData, ...this.currentScope }; this.comms.sendToApp('new_formation', payload); } /** * Select a formation for editing. * @param {string} tblKey - Formation tbl_key */ selectFormation(tblKey) { console.log("Selecting formation:", tblKey); if (this.overlay) { this.overlay.selectFormation(tblKey); } } /** * Delete a formation. * @param {string} tblKey - Formation tbl_key * @param {string} name - Formation name (for confirmation) */ deleteFormation(tblKey, name) { if (!confirm(`Delete formation "${name}"?`)) { return; } if (this.comms) { this.comms.sendToApp('delete_formation', { tbl_key: tblKey }); } } /** * Update a formation's lines (called from overlay after drag). * @param {string} tblKey - Formation tbl_key * @param {Object} linesData - Updated lines data */ updateFormationLines(tblKey, linesData) { if (!this.comms) return; this.comms.sendToApp('edit_formation', { tbl_key: tblKey, lines_json: JSON.stringify(linesData) }); } }